mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-11 16:32:57 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 995cd3d84c | |||
| 919cdf0075 | |||
| d181c9cfc1 | |||
| 5943142c44 | |||
| dbc430e838 | |||
| 7413d03d31 | |||
| dd60067684 | |||
| 04e4ddf525 | |||
| 99bf6d7c75 | |||
| 99a203933c | |||
| b43d2a2040 | |||
| ba9c406def | |||
| f5f4d50cc9 | |||
| f87506b490 | |||
| f6f9a53ce5 | |||
| 61130ef54e | |||
| e8bcbd59be | |||
| d433ff7475 | |||
| 71957037d0 | |||
| bcdcdbbd4e | |||
| 1ece13f5bc | |||
| da70ba6ec7 | |||
| a832f0e4d4 | |||
| 6ab4d005a0 | |||
| 18dc935038 | |||
| bb3a534f75 | |||
| 649ba370c6 | |||
| 51732c5ce6 | |||
| 0ba7b46680 | |||
| 94dfbb7b9e | |||
| b8b7512b35 | |||
| aba6b773ce | |||
| d88fb0d356 | |||
| b8c6d55156 |
@@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||
<PackageVersion>9.7.0</PackageVersion>
|
||||
<AssemblyVersion>9.7.0</AssemblyVersion>
|
||||
<FileVersion>9.7.0</FileVersion>
|
||||
<PackageVersion>9.11.1</PackageVersion>
|
||||
<AssemblyVersion>9.11.1</AssemblyVersion>
|
||||
<FileVersion>9.11.1</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
@@ -41,7 +41,7 @@
|
||||
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CryptoExchange.Net" Version="9.7.0" />
|
||||
<PackageReference Include="CryptoExchange.Net" Version="9.11.0" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -5,6 +5,21 @@
|
||||
Protobuf support for CryptoExchange.Net.
|
||||
|
||||
## Release notes
|
||||
* Version 9.11.1 - 30 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.11.0 - 30 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.10.0 - 15 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.9.0 - 06 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.8.0 - 30 Sep 2025
|
||||
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.7.0 - 01 Sep 2025
|
||||
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"></PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"></PackageReference>
|
||||
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0"></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
|
||||
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
|
||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
|
||||
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
|
||||
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
||||
|
||||
@@ -202,7 +202,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
await sub;
|
||||
|
||||
// assert
|
||||
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
|
||||
ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
@@ -225,7 +225,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
await sub;
|
||||
|
||||
// assert
|
||||
Assert.That(client.SubClient.TestSubscription.Confirmed);
|
||||
Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ namespace CryptoExchange.Net.Clients
|
||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||
{
|
||||
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
|
||||
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace CryptoExchange.Net.Clients
|
||||
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||
{
|
||||
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
|
||||
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -227,7 +227,7 @@ namespace CryptoExchange.Net.Clients
|
||||
while (true)
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<UpdateSubscription>(null);
|
||||
|
||||
@@ -269,6 +269,7 @@ namespace CryptoExchange.Net.Clients
|
||||
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
||||
}
|
||||
|
||||
subscription.Status = SubscriptionStatus.Subscribing;
|
||||
var waitEvent = new AsyncResetEvent(false);
|
||||
var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
|
||||
if (subQuery != null)
|
||||
@@ -279,7 +280,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
waitEvent?.Set();
|
||||
var isTimeout = subResult.Error is CancellationRequestedError;
|
||||
if (isTimeout && subscription.Confirmed)
|
||||
if (isTimeout && subscription.Status == SubscriptionStatus.Subscribed)
|
||||
{
|
||||
// No response received, but the subscription did receive updates. We'll assume success
|
||||
}
|
||||
@@ -287,6 +288,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
||||
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
||||
subscription.Status = SubscriptionStatus.Pending;
|
||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||
}
|
||||
@@ -295,7 +297,7 @@ namespace CryptoExchange.Net.Clients
|
||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||
}
|
||||
|
||||
subscription.Confirmed = true;
|
||||
subscription.Status = SubscriptionStatus.Subscribed;
|
||||
if (ct != default)
|
||||
{
|
||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
||||
@@ -343,7 +345,7 @@ namespace CryptoExchange.Net.Clients
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
|
||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<THandlerResponse>(default);
|
||||
|
||||
@@ -494,25 +496,56 @@ 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="ct">Cancellation token</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, string? topic = null)
|
||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, CancellationToken ct, 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('/')
|
||||
var socketQuery = socketConnections.Where(s => 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);
|
||||
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
|
||||
.Select(x => x.Value)
|
||||
.ToList();
|
||||
|
||||
SocketConnection connection;
|
||||
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
|
||||
var delayStart = DateTime.UtcNow;
|
||||
var delayed = false;
|
||||
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
|
||||
{
|
||||
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
|
||||
{
|
||||
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketConnection.SocketStatus.Reconnecting || x.Status == SocketConnection.SocketStatus.Resubscribing))
|
||||
{
|
||||
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
|
||||
_logger.TimeoutWaitingForReconnectingSocket();
|
||||
return new CallResult<SocketConnection>(new CantConnectError());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
delayed = true;
|
||||
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
|
||||
|
||||
if (ct.IsCancellationRequested)
|
||||
return new CallResult<SocketConnection>(new CancellationRequestedError());
|
||||
}
|
||||
|
||||
if (delayed)
|
||||
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
|
||||
|
||||
socketQuery = socketQuery.Where(s => (s.Status == SocketConnection.SocketStatus.None || s.Status == SocketConnection.SocketStatus.Connected)
|
||||
&& (s.Authenticated == authenticated || !authenticated)
|
||||
&& s.Connected).ToList();
|
||||
|
||||
SocketConnection? connection;
|
||||
if (!dedicatedRequestConnection)
|
||||
{
|
||||
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
||||
connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
|
||||
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
|
||||
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
||||
// Mark dedicated request connection as authenticated if the request is authenticated
|
||||
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
||||
@@ -520,9 +553,12 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
if (connection != null)
|
||||
{
|
||||
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
||||
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget
|
||||
|| (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
|
||||
{
|
||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||
return new CallResult<SocketConnection>(connection);
|
||||
}
|
||||
}
|
||||
|
||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||
@@ -716,7 +752,7 @@ namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
foreach (var item in DedicatedConnectionConfigs)
|
||||
{
|
||||
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
|
||||
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.AsDataless();
|
||||
|
||||
@@ -813,7 +849,7 @@ namespace CryptoExchange.Net.Clients
|
||||
cs.SubscriptionStates.ForEach(subState =>
|
||||
{
|
||||
sb.AppendLine($"\t\t\tId: {subState.Id}");
|
||||
sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
|
||||
sb.AppendLine($"\t\t\tStatus: {subState.Status}");
|
||||
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
|
||||
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
@@ -47,7 +48,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
if (typeToConvert == typeof(bool))
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool");
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Received null bool value, but property type is not a nullable bool");
|
||||
return default;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
@@ -14,8 +15,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
|
||||
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
|
||||
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
|
||||
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
@@ -39,17 +40,17 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
if (typeToConvert == typeof(DateTime))
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable");
|
||||
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable");
|
||||
return default;
|
||||
}
|
||||
|
||||
if (reader.TokenType is JsonTokenType.Number)
|
||||
{
|
||||
var longValue = reader.GetDouble();
|
||||
if (longValue == 0 || longValue < 0)
|
||||
var decValue = reader.GetDecimal();
|
||||
if (decValue == 0 || decValue < 0)
|
||||
return default;
|
||||
|
||||
return ParseFromDouble(longValue);
|
||||
return ParseFromDecimal(decValue);
|
||||
}
|
||||
else if (reader.TokenType is JsonTokenType.String)
|
||||
{
|
||||
@@ -57,7 +58,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (string.IsNullOrWhiteSpace(stringValue)
|
||||
|| stringValue == "-1"
|
||||
|| stringValue == "0001-01-01T00:00:00Z"
|
||||
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
|
||||
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
@@ -88,20 +89,24 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a long value to datetime
|
||||
/// Parse a double value to datetime
|
||||
/// </summary>
|
||||
/// <param name="longValue"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ParseFromDouble(double longValue)
|
||||
{
|
||||
if (longValue < 19999999999)
|
||||
return ConvertFromSeconds(longValue);
|
||||
if (longValue < 19999999999999)
|
||||
return ConvertFromMilliseconds(longValue);
|
||||
if (longValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds(longValue);
|
||||
public static DateTime ParseFromDouble(double value)
|
||||
=> ParseFromDecimal((decimal)value);
|
||||
|
||||
return ConvertFromNanoseconds(longValue);
|
||||
/// <summary>
|
||||
/// Parse a decimal value to datetime
|
||||
/// </summary>
|
||||
public static DateTime ParseFromDecimal(decimal value)
|
||||
{
|
||||
if (value < 19999999999)
|
||||
return ConvertFromSeconds(value);
|
||||
if (value < 19999999999999)
|
||||
return ConvertFromMilliseconds(value);
|
||||
if (value < 19999999999999999)
|
||||
return ConvertFromMicroseconds(value);
|
||||
|
||||
return ConvertFromNanoseconds(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -120,7 +125,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
||||
{
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: " + stringValue);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
||||
@@ -133,7 +138,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
||||
{
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: " + stringValue);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
@@ -146,25 +151,25 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
||||
{
|
||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: " + stringValue);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
if (decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalValue))
|
||||
{
|
||||
// Parse 1637745563.000 format
|
||||
if (doubleValue <= 0)
|
||||
if (decimalValue <= 0)
|
||||
return default;
|
||||
if (doubleValue < 19999999999)
|
||||
return ConvertFromSeconds(doubleValue);
|
||||
if (doubleValue < 19999999999999)
|
||||
return ConvertFromMilliseconds((long)doubleValue);
|
||||
if (doubleValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds((long)doubleValue);
|
||||
if (decimalValue < 19999999999)
|
||||
return ConvertFromSeconds(decimalValue);
|
||||
if (decimalValue < 19999999999999)
|
||||
return ConvertFromMilliseconds(decimalValue);
|
||||
if (decimalValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds(decimalValue);
|
||||
|
||||
return ConvertFromNanoseconds((long)doubleValue);
|
||||
return ConvertFromNanoseconds(decimalValue);
|
||||
}
|
||||
|
||||
if (stringValue.Length == 10)
|
||||
@@ -175,7 +180,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|| !int.TryParse(values[1], out var month)
|
||||
|| !int.TryParse(values[2], out var day))
|
||||
{
|
||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: " + stringValue);
|
||||
return default;
|
||||
}
|
||||
|
||||
@@ -188,54 +193,70 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <summary>
|
||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="seconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
||||
/// <summary>
|
||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="milliseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
||||
/// <summary>
|
||||
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="microseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
||||
public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="nanoseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
||||
public static DateTime ConvertFromSeconds(double seconds) => ConvertFromSeconds((decimal)seconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromSeconds(long seconds) => ConvertFromSeconds((decimal)seconds);
|
||||
/// <summary>
|
||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMilliseconds(decimal milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMilliseconds(double milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMilliseconds(long milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||
/// <summary>
|
||||
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMicroseconds(decimal microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMicroseconds(double microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMicroseconds(long microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromNanoseconds(decimal nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromNanoseconds(double nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromNanoseconds(long nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
@@ -79,7 +81,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
}
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
||||
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||
@@ -98,20 +100,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
/// <inheritdoc />
|
||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
||||
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
||||
if (t == null)
|
||||
{
|
||||
if (warn)
|
||||
if (isEmptyString && !_unknownValuesWarned.Contains(null))
|
||||
{
|
||||
if (isEmptyString)
|
||||
{
|
||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
||||
}
|
||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
||||
}
|
||||
|
||||
return new T(); // return default value
|
||||
@@ -122,10 +117,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
}
|
||||
}
|
||||
|
||||
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString, out bool warn)
|
||||
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString)
|
||||
{
|
||||
isEmptyString = false;
|
||||
warn = false;
|
||||
var enumType = typeof(T);
|
||||
if (_mapping == null)
|
||||
_mapping = AddMapping();
|
||||
@@ -140,10 +134,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(stringValue))
|
||||
if (stringValue is null)
|
||||
return null;
|
||||
|
||||
if (!GetValue(enumType, stringValue!, out var result))
|
||||
if (!GetValue(enumType, stringValue, out var result))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
{
|
||||
@@ -154,9 +148,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
// We received an enum value but weren't able to parse it.
|
||||
if (!_unknownValuesWarned.Contains(stringValue))
|
||||
{
|
||||
warn = true;
|
||||
_unknownValuesWarned.Add(stringValue!);
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
||||
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +197,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return false;
|
||||
}
|
||||
|
||||
if (String.IsNullOrEmpty(value))
|
||||
{
|
||||
// An empty/null value will always fail when parsing, so just return here
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse 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>9.7.0</PackageVersion>
|
||||
<AssemblyVersion>9.7.0</AssemblyVersion>
|
||||
<FileVersion>9.7.0</FileVersion>
|
||||
<PackageVersion>9.11.0</PackageVersion>
|
||||
<AssemblyVersion>9.11.0</AssemblyVersion>
|
||||
<FileVersion>9.11.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;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
@@ -51,11 +51,11 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.10" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Client Packages">
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -88,8 +88,6 @@ namespace CryptoExchange.Net
|
||||
value -= offset;
|
||||
else value += (step.Value - offset);
|
||||
}
|
||||
|
||||
value = RoundDown(value, 8);
|
||||
|
||||
return value.Normalize();
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@ namespace CryptoExchange.Net
|
||||
/// <summary>
|
||||
/// Whether the trading mode is linear
|
||||
/// </summary>
|
||||
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
||||
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the trading mode is inverse
|
||||
@@ -416,6 +416,36 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the account type is a futures account
|
||||
/// </summary>
|
||||
public static bool IsFuturesAccount(this SharedAccountType type) =>
|
||||
type == SharedAccountType.PerpetualLinearFutures
|
||||
|| type == SharedAccountType.DeliveryLinearFutures
|
||||
|| type == SharedAccountType.PerpetualInverseFutures
|
||||
|| type == SharedAccountType.DeliveryInverseFutures;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the account type is a margin account
|
||||
/// </summary>
|
||||
public static bool IsMarginAccount(this SharedAccountType type) =>
|
||||
type == SharedAccountType.CrossMargin
|
||||
|| type == SharedAccountType.IsolatedMargin;
|
||||
|
||||
/// <summary>
|
||||
/// Map a TradingMode value to a SharedAccountType enum value
|
||||
/// </summary>
|
||||
public static SharedAccountType ToAccountType(this TradingMode mode)
|
||||
{
|
||||
if (mode == TradingMode.Spot) return SharedAccountType.Spot;
|
||||
if (mode == TradingMode.PerpetualLinear) return SharedAccountType.PerpetualLinearFutures;
|
||||
if (mode == TradingMode.PerpetualInverse) return SharedAccountType.PerpetualInverseFutures;
|
||||
if (mode == TradingMode.DeliveryInverse) return SharedAccountType.DeliveryInverseFutures;
|
||||
if (mode == TradingMode.DeliveryLinear) return SharedAccountType.DeliveryLinearFutures;
|
||||
|
||||
throw new ArgumentException(nameof(mode), "Unmapped trading mode");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register rest client interfaces
|
||||
/// </summary>
|
||||
@@ -445,6 +475,8 @@ namespace CryptoExchange.Net
|
||||
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
||||
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
||||
if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ITransferRestClient)client(x)!);
|
||||
|
||||
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Trackers.Klines;
|
||||
using CryptoExchange.Net.Trackers.Trades;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracker factory
|
||||
/// </summary>
|
||||
public interface ITrackerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the factory supports creating a KlineTracker instance for this symbol and interval
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol</param>
|
||||
/// <param name="interval">The kline interval</param>
|
||||
bool CanCreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new kline tracker
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol</param>
|
||||
/// <param name="interval">Kline interval</param>
|
||||
/// <param name="limit">The max amount of klines to retain</param>
|
||||
/// <param name="period">The max period the data should be retained</param>
|
||||
/// <returns></returns>
|
||||
IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the factory supports creating a TradeTracker instance for this symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol</param>
|
||||
bool CanCreateTradeTracker(SharedSymbol symbol);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new trade tracker for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol</param>
|
||||
/// <param name="limit">The max amount of trades to retain</param>
|
||||
/// <param name="period">The max period the data should be retained</param>
|
||||
/// <returns></returns>
|
||||
ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
@@ -12,11 +13,50 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public static class LibraryHelpers
|
||||
{
|
||||
private static ILogger? _staticLogger;
|
||||
/// <summary>
|
||||
/// Static logger
|
||||
/// </summary>
|
||||
public static ILogger? StaticLogger
|
||||
{
|
||||
get => _staticLogger;
|
||||
internal set
|
||||
{
|
||||
if (_staticLogger != null)
|
||||
return;
|
||||
|
||||
_staticLogger = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client order id separator
|
||||
/// </summary>
|
||||
public const string ClientOrderIdSeparator = "JK";
|
||||
|
||||
private static Dictionary<string, string> _defaultClientReferences = new Dictionary<string, string>()
|
||||
{
|
||||
{ "Binance.Spot", "x-VICEW9VV" },
|
||||
{ "Binance.Futures", "x-d63tKbx3" },
|
||||
{ "BingX", "easytrading" },
|
||||
{ "Bitfinex", "kCCe-CNBO" },
|
||||
{ "Bitget", "6x21p" },
|
||||
{ "BitMart", "EASYTRADING0001" },
|
||||
{ "BitMEX", "Sent from JKorf" },
|
||||
{ "BloFin", "5c07cf695885c282" },
|
||||
{ "Bybit", "Zx000356" },
|
||||
{ "CoinEx", "x-147866029-" },
|
||||
{ "GateIo", "copytraderpw" },
|
||||
{ "HTX", "AA1ef14811" },
|
||||
{ "Kucoin.FuturesName", "Easytradingfutures" },
|
||||
{ "Kucoin.FuturesKey", "9e08c05f-454d-4580-82af-2f4c7027fd00" },
|
||||
{ "Kucoin.SpotName", "Easytrading" },
|
||||
{ "Kucoin.SpotKey", "f8ae62cb-2b3d-420c-8c98-e1c17dd4e30a" },
|
||||
{ "Mexc", "EASYT" },
|
||||
{ "OKX", "1425d83a94fbBCDE" },
|
||||
{ "XT", "4XWeqN10M1fcoI5L" },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Apply broker id to a client order id
|
||||
/// </summary>
|
||||
@@ -47,6 +87,22 @@ namespace CryptoExchange.Net
|
||||
return clientOrderId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the client reference for an exchange if available
|
||||
/// </summary>
|
||||
public static string GetClientReference(Func<string?> optionsReference, string exchange, string? topic = null)
|
||||
{
|
||||
var optionsValue = optionsReference();
|
||||
if (!string.IsNullOrEmpty(optionsValue))
|
||||
return optionsValue!;
|
||||
|
||||
var key = exchange;
|
||||
if (topic != null)
|
||||
key += "." + topic;
|
||||
|
||||
return _defaultClientReferences.TryGetValue(key, out var id) ? id : throw new KeyNotFoundException($"{exchange} not found in configuration");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new HttpMessageHandler instance
|
||||
/// </summary>
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
private static readonly Action<ILogger, int, int, Exception?> _unsubscribingSubscription;
|
||||
private static readonly Action<ILogger, int, Exception?> _reconnectingAllConnections;
|
||||
private static readonly Action<ILogger, DateTime, Exception?> _addingRetryAfterGuard;
|
||||
private static readonly Action<ILogger, Exception?> _timeoutWaitingForReconnectingSocket;
|
||||
private static readonly Action<ILogger, long, Exception?> _waitedForReconnectingSocket;
|
||||
|
||||
static SocketApiClientLoggingExtension()
|
||||
{
|
||||
@@ -110,6 +112,16 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
LogLevel.Warning,
|
||||
new EventId(3018, "AddRetryAfterGuard"),
|
||||
"Adding RetryAfterGuard ({RetryAfter}) because the connection attempt was rate limited");
|
||||
|
||||
_timeoutWaitingForReconnectingSocket = LoggerMessage.Define(
|
||||
LogLevel.Debug,
|
||||
new EventId(3019, "TimeoutWaitingForReconnectingSocket"),
|
||||
"Timeout while waiting for existing socket reconnection, failing request");
|
||||
|
||||
_waitedForReconnectingSocket = LoggerMessage.Define<long>(
|
||||
LogLevel.Trace,
|
||||
new EventId(3020, "WaitedForReconnectingSocket"),
|
||||
"Waited for reconnecting socket for {Timespan}ms");
|
||||
}
|
||||
|
||||
public static void FailedToAddSubscriptionRetryOnDifferentConnection(this ILogger logger, int socketId)
|
||||
@@ -196,5 +208,14 @@ namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
_addingRetryAfterGuard(logger, retryAfter, null);
|
||||
}
|
||||
|
||||
public static void TimeoutWaitingForReconnectingSocket(this ILogger logger)
|
||||
{
|
||||
_timeoutWaitingForReconnectingSocket(logger, null);
|
||||
}
|
||||
public static void WaitedForReconnectingSocket(this ILogger logger, long milliseconds)
|
||||
{
|
||||
_waitedForReconnectingSocket(logger, milliseconds, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,4 +267,31 @@ namespace CryptoExchange.Net.Objects
|
||||
Succeed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscription status
|
||||
/// </summary>
|
||||
public enum SubscriptionStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Pending, waiting before (re)subscription can be started
|
||||
/// </summary>
|
||||
Pending,
|
||||
/// <summary>
|
||||
/// Currently (re)subscribing, will start producing updates soon if subscription is successful
|
||||
/// </summary>
|
||||
Subscribing,
|
||||
/// <summary>
|
||||
/// Subscribed and listening to updates
|
||||
/// </summary>
|
||||
Subscribed,
|
||||
/// <summary>
|
||||
/// Subscription is being closed and will stop producing updates
|
||||
/// </summary>
|
||||
Closing,
|
||||
/// <summary>
|
||||
/// Subscription is closed and will no long produce updates
|
||||
/// </summary>
|
||||
Closed
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Sockets
|
||||
@@ -12,13 +14,27 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
private readonly SocketConnection _connection;
|
||||
private readonly Subscription _listener;
|
||||
|
||||
private object _eventLock = new object();
|
||||
private bool _connectionEventsSubscribed = true;
|
||||
private List<Action> _connectionClosedEventHandlers = new List<Action>();
|
||||
private List<Action> _connectionLostEventHandlers = new List<Action>();
|
||||
private List<Action<Error>> _resubscribeFailedEventHandlers = new List<Action<Error>>();
|
||||
private List<Action<TimeSpan>> _connectionRestoredEventHandlers = new List<Action<TimeSpan>>();
|
||||
private List<Action> _activityPausedEventHandlers = new List<Action>();
|
||||
private List<Action> _activityUnpausedEventHandlers = new List<Action>();
|
||||
|
||||
/// <summary>
|
||||
/// Event when the status of the subscription changes
|
||||
/// </summary>
|
||||
public event Action<SubscriptionStatus>? SubscriptionStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event when the connection is lost. The socket will automatically reconnect when possible.
|
||||
/// </summary>
|
||||
public event Action ConnectionLost
|
||||
{
|
||||
add => _connection.ConnectionLost += value;
|
||||
remove => _connection.ConnectionLost -= value;
|
||||
add { lock (_eventLock) _connectionLostEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _connectionLostEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -26,8 +42,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public event Action ConnectionClosed
|
||||
{
|
||||
add => _connection.ConnectionClosed += value;
|
||||
remove => _connection.ConnectionClosed -= value;
|
||||
add { lock (_eventLock) _connectionClosedEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _connectionClosedEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -35,8 +51,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public event Action<Error> ResubscribingFailed
|
||||
{
|
||||
add => _connection.ResubscribingFailed += value;
|
||||
remove => _connection.ResubscribingFailed -= value;
|
||||
add { lock (_eventLock) _resubscribeFailedEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _resubscribeFailedEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -46,8 +62,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public event Action<TimeSpan> ConnectionRestored
|
||||
{
|
||||
add => _connection.ConnectionRestored += value;
|
||||
remove => _connection.ConnectionRestored -= value;
|
||||
add { lock (_eventLock) _connectionRestoredEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _connectionRestoredEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -55,8 +71,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public event Action ActivityPaused
|
||||
{
|
||||
add => _connection.ActivityPaused += value;
|
||||
remove => _connection.ActivityPaused -= value;
|
||||
add { lock (_eventLock) _activityPausedEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _activityPausedEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,8 +80,8 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public event Action ActivityUnpaused
|
||||
{
|
||||
add => _connection.ActivityUnpaused += value;
|
||||
remove => _connection.ActivityUnpaused -= value;
|
||||
add { lock (_eventLock) _activityUnpausedEventHandlers.Add(value); }
|
||||
remove { lock (_eventLock) _activityUnpausedEventHandlers.Remove(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -95,7 +111,128 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
public UpdateSubscription(SocketConnection connection, Subscription subscription)
|
||||
{
|
||||
_connection = connection;
|
||||
_connection.ConnectionClosed += HandleConnectionClosedEvent;
|
||||
_connection.ConnectionLost += HandleConnectionLostEvent;
|
||||
_connection.ConnectionRestored += HandleConnectionRestoredEvent;
|
||||
_connection.ResubscribingFailed += HandleResubscribeFailedEvent;
|
||||
_connection.ActivityPaused += HandlePausedEvent;
|
||||
_connection.ActivityUnpaused += HandleUnpausedEvent;
|
||||
|
||||
_listener = subscription;
|
||||
_listener.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
|
||||
}
|
||||
|
||||
private void UnsubscribeConnectionEvents()
|
||||
{
|
||||
lock (_eventLock)
|
||||
{
|
||||
if (!_connectionEventsSubscribed)
|
||||
return;
|
||||
|
||||
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
|
||||
_connection.ConnectionLost -= HandleConnectionLostEvent;
|
||||
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
|
||||
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
|
||||
_connection.ActivityPaused -= HandlePausedEvent;
|
||||
_connection.ActivityUnpaused -= HandleUnpausedEvent;
|
||||
_connectionEventsSubscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectionClosedEvent()
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
|
||||
// If we're not the subscription closing this connection don't bother emitting
|
||||
if (!_listener.IsClosingConnection)
|
||||
return;
|
||||
|
||||
List<Action> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _connectionClosedEventHandlers.ToList();
|
||||
|
||||
foreach(var callback in handlers)
|
||||
callback();
|
||||
}
|
||||
|
||||
private void HandleConnectionLostEvent()
|
||||
{
|
||||
if (!_listener.Active)
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Action> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _connectionLostEventHandlers.ToList();
|
||||
|
||||
foreach (var callback in handlers)
|
||||
callback();
|
||||
}
|
||||
|
||||
private void HandleConnectionRestoredEvent(TimeSpan period)
|
||||
{
|
||||
if (!_listener.Active)
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Action<TimeSpan>> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _connectionRestoredEventHandlers.ToList();
|
||||
|
||||
foreach (var callback in handlers)
|
||||
callback(period);
|
||||
}
|
||||
|
||||
private void HandleResubscribeFailedEvent(Error error)
|
||||
{
|
||||
if (!_listener.Active)
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Action<Error>> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _resubscribeFailedEventHandlers.ToList();
|
||||
|
||||
foreach (var callback in handlers)
|
||||
callback(error);
|
||||
}
|
||||
|
||||
private void HandlePausedEvent()
|
||||
{
|
||||
if (!_listener.Active)
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Action> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _activityPausedEventHandlers.ToList();
|
||||
|
||||
foreach (var callback in handlers)
|
||||
callback();
|
||||
}
|
||||
|
||||
private void HandleUnpausedEvent()
|
||||
{
|
||||
if (!_listener.Active)
|
||||
{
|
||||
UnsubscribeConnectionEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Action> handlers;
|
||||
lock (_eventLock)
|
||||
handlers = _activityUnpausedEventHandlers.ToList();
|
||||
|
||||
foreach (var callback in handlers)
|
||||
callback();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Account type
|
||||
/// </summary>
|
||||
public enum SharedAccountType
|
||||
{
|
||||
/// <summary>
|
||||
/// Unified account, combined account for multiple different types of trading
|
||||
/// </summary>
|
||||
Unified,
|
||||
|
||||
/// <summary>
|
||||
/// Funding account, where withdrawals and deposits are made from and to
|
||||
/// </summary>
|
||||
Funding,
|
||||
|
||||
/// <summary>
|
||||
/// Spot trading account
|
||||
/// </summary>
|
||||
Spot,
|
||||
|
||||
/// <summary>
|
||||
/// Cross margin account
|
||||
/// </summary>
|
||||
CrossMargin,
|
||||
/// <summary>
|
||||
/// Isolated margin account
|
||||
/// </summary>
|
||||
IsolatedMargin,
|
||||
|
||||
/// <summary>
|
||||
/// Perpetual linear futures account
|
||||
/// </summary>
|
||||
PerpetualLinearFutures,
|
||||
/// <summary>
|
||||
/// Delivery linear futures account
|
||||
/// </summary>
|
||||
DeliveryLinearFutures,
|
||||
/// <summary>
|
||||
/// Perpetual inverse futures account
|
||||
/// </summary>
|
||||
PerpetualInverseFutures,
|
||||
/// <summary>
|
||||
/// Delivery inverse futures account
|
||||
/// </summary>
|
||||
DeliveryInverseFutures,
|
||||
|
||||
/// <summary>
|
||||
/// Option account
|
||||
/// </summary>
|
||||
Option,
|
||||
|
||||
/// <summary>
|
||||
/// Other
|
||||
/// </summary>
|
||||
Other
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Balances request options
|
||||
/// </summary>
|
||||
EndpointOptions<GetBalancesRequest> GetBalancesOptions { get; }
|
||||
GetBalancesOptions GetBalancesOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get balances for the user
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Client for transferring funds between account types
|
||||
/// </summary>
|
||||
public interface ITransferRestClient : ISharedClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Transfer request options
|
||||
/// </summary>
|
||||
TransferOptions TransferOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Transfer funds between account types
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedId>> TransferAsync(TransferRequest request, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for requesting a transfer
|
||||
/// </summary>
|
||||
public class GetBalancesOptions : EndpointOptions<GetBalancesRequest>
|
||||
{
|
||||
/// <summary>
|
||||
/// Supported account types
|
||||
/// </summary>
|
||||
public AccountTypeFilter[] SupportedAccountTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetBalancesOptions(params AccountTypeFilter[] accountTypes) : base(true)
|
||||
{
|
||||
SupportedAccountTypes = accountTypes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a request
|
||||
/// </summary>
|
||||
public Error? ValidateRequest(
|
||||
string exchange,
|
||||
GetBalancesRequest request,
|
||||
TradingMode[] supportedApiTypes)
|
||||
{
|
||||
if (request.AccountType != null && !IsValid(request.AccountType.Value))
|
||||
return ArgumentError.Invalid(nameof(request.AccountType), "Invalid AccountType");
|
||||
|
||||
return base.ValidateRequest(exchange, request, null, supportedApiTypes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the account type valid for this client
|
||||
/// </summary>
|
||||
/// <param name="accountType"></param>
|
||||
/// <returns></returns>
|
||||
public bool IsValid(SharedAccountType accountType)
|
||||
{
|
||||
if (accountType == SharedAccountType.Funding)
|
||||
return SupportedAccountTypes.Contains(AccountTypeFilter.Funding);
|
||||
|
||||
if (accountType == SharedAccountType.Spot)
|
||||
return SupportedAccountTypes.Contains(AccountTypeFilter.Spot);
|
||||
|
||||
if (accountType == SharedAccountType.PerpetualLinearFutures
|
||||
|| accountType == SharedAccountType.PerpetualInverseFutures
|
||||
|| accountType == SharedAccountType.DeliveryLinearFutures
|
||||
|| accountType == SharedAccountType.DeliveryInverseFutures)
|
||||
{
|
||||
return SupportedAccountTypes.Contains(AccountTypeFilter.Futures);
|
||||
}
|
||||
|
||||
|
||||
if (accountType == SharedAccountType.CrossMargin
|
||||
|| accountType == SharedAccountType.IsolatedMargin)
|
||||
{
|
||||
return SupportedAccountTypes.Contains(AccountTypeFilter.Margin);
|
||||
}
|
||||
|
||||
return SupportedAccountTypes.Contains(AccountTypeFilter.Option);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Account type filter
|
||||
/// </summary>
|
||||
public enum AccountTypeFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Funding account
|
||||
/// </summary>
|
||||
Funding,
|
||||
/// <summary>
|
||||
/// Spot account
|
||||
/// </summary>
|
||||
Spot,
|
||||
/// <summary>
|
||||
/// Futures account
|
||||
/// </summary>
|
||||
Futures,
|
||||
/// <summary>
|
||||
/// Margin account
|
||||
/// </summary>
|
||||
Margin,
|
||||
/// <summary>
|
||||
/// Option account
|
||||
/// </summary>
|
||||
Option
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for requesting a transfer
|
||||
/// </summary>
|
||||
public class TransferOptions : EndpointOptions<TransferRequest>
|
||||
{
|
||||
/// <summary>
|
||||
/// Supported account types
|
||||
/// </summary>
|
||||
public SharedAccountType[] SupportedAccountTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TransferOptions(SharedAccountType[] accountTypes) : base(true)
|
||||
{
|
||||
SupportedAccountTypes = accountTypes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a request
|
||||
/// </summary>
|
||||
public new Error? ValidateRequest(
|
||||
string exchange,
|
||||
TransferRequest request,
|
||||
TradingMode? tradingMode,
|
||||
TradingMode[] supportedApiTypes)
|
||||
{
|
||||
if (!SupportedAccountTypes.Contains(request.FromAccountType))
|
||||
return ArgumentError.Invalid(nameof(request.FromAccountType), "Invalid FromAccountType");
|
||||
|
||||
if (!SupportedAccountTypes.Contains(request.ToAccountType))
|
||||
return ArgumentError.Invalid(nameof(request.FromAccountType), "Invalid ToAccountType");
|
||||
|
||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,28 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public record GetBalancesRequest : SharedRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Trading mode
|
||||
/// Account type
|
||||
/// </summary>
|
||||
public TradingMode? TradingMode { get; set; }
|
||||
public SharedAccountType? AccountType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="tradingMode">Trading mode</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetBalancesRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
public GetBalancesRequest(TradingMode tradingMode, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
{
|
||||
TradingMode = tradingMode;
|
||||
AccountType = tradingMode.ToAccountType();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="accountType">Account type</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetBalancesRequest(SharedAccountType? accountType = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
{
|
||||
AccountType = accountType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Request to transfer funds between account types
|
||||
/// </summary>
|
||||
public record TransferRequest : SharedRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Asset
|
||||
/// </summary>
|
||||
public string Asset { get; set; }
|
||||
/// <summary>
|
||||
/// Quantity
|
||||
/// </summary>
|
||||
public decimal Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// From symbol
|
||||
/// </summary>
|
||||
public string? FromSymbol { get; set; }
|
||||
/// <summary>
|
||||
/// To symbol
|
||||
/// </summary>
|
||||
public string? ToSymbol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// From account type
|
||||
/// </summary>
|
||||
public SharedAccountType FromAccountType { get; set; }
|
||||
/// <summary>
|
||||
/// To account type
|
||||
/// </summary>
|
||||
public SharedAccountType ToAccountType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="asset">The asset to transfer</param>
|
||||
/// <param name="quantity">Quantity to transfer</param>
|
||||
/// <param name="fromAccount">From account type</param>
|
||||
/// <param name="toAccount">To account type</param>
|
||||
/// <param name="fromSymbol">From symbol</param>
|
||||
/// <param name="toSymbol">To symbol</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public TransferRequest(
|
||||
string asset,
|
||||
decimal quantity,
|
||||
SharedAccountType fromAccount,
|
||||
SharedAccountType toAccount,
|
||||
string? fromSymbol = null,
|
||||
string? toSymbol = null,
|
||||
ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||
{
|
||||
Asset = asset;
|
||||
Quantity = quantity;
|
||||
FromAccountType = fromAccount;
|
||||
ToAccountType = toAccount;
|
||||
FromSymbol = fromSymbol;
|
||||
ToSymbol = toSymbol;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Min number of confirmations
|
||||
/// </summary>
|
||||
public int? MinConfirmations { get; set; }
|
||||
/// <summary>
|
||||
/// The contract address
|
||||
/// </summary>
|
||||
public string? ContractAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
/// Total quantity
|
||||
/// </summary>
|
||||
public decimal Total { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Isolated margin symbol, only applicable for isolated margin futures
|
||||
/// </summary>
|
||||
|
||||
@@ -28,6 +28,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public string OrderId { get; set; }
|
||||
/// <summary>
|
||||
/// The client order id
|
||||
/// </summary>
|
||||
public string? ClientOrderId { get; set; }
|
||||
/// <summary>
|
||||
/// Side of the trade
|
||||
/// </summary>
|
||||
public SharedOrderSide? Side { get; set; }
|
||||
|
||||
@@ -296,7 +296,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
lock (_listenersLock)
|
||||
{
|
||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
|
||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription && !l.IsClosingConnection))
|
||||
subscription.Reset();
|
||||
|
||||
foreach (var query in _listeners.OfType<Query>().ToList())
|
||||
@@ -527,10 +527,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed)
|
||||
if (processor is Subscription subscriptionProcessor && subscriptionProcessor.Status == SubscriptionStatus.Subscribing)
|
||||
{
|
||||
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
|
||||
subscriptionProcessor.Confirmed = true;
|
||||
subscriptionProcessor.Status = SubscriptionStatus.Subscribed;
|
||||
if (subscriptionProcessor.SubscriptionQuery?.TimeoutBehavior == TimeoutBehavior.Succeed)
|
||||
// If this subscription has a query waiting for a timeout (success if there is no error response)
|
||||
// then time it out now as the data is being received, so we assume it's successful
|
||||
@@ -657,13 +657,16 @@ namespace CryptoExchange.Net.Sockets
|
||||
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)
|
||||
while (subscription.Status == SubscriptionStatus.Subscribing)
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
|
||||
subscription.Closed = true;
|
||||
subscription.Status = SubscriptionStatus.Closing;
|
||||
|
||||
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
|
||||
{
|
||||
subscription.Status = SubscriptionStatus.Closed;
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.ClosingSubscription(SocketId, subscription.Id);
|
||||
if (subscription.CancellationTokenRegistration.HasValue)
|
||||
@@ -675,7 +678,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
bool shouldCloseConnection;
|
||||
lock (_listenersLock)
|
||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
|
||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
|
||||
|
||||
if (!anyDuplicateSubscription)
|
||||
{
|
||||
@@ -693,6 +696,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
if (Status == SocketStatus.Closing)
|
||||
{
|
||||
subscription.Status = SubscriptionStatus.Closed;
|
||||
_logger.AlreadyClosing(SocketId);
|
||||
return;
|
||||
}
|
||||
@@ -700,12 +704,15 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (shouldCloseConnection)
|
||||
{
|
||||
Status = SocketStatus.Closing;
|
||||
subscription.IsClosingConnection = true;
|
||||
_logger.ClosingNoMoreSubscriptions(SocketId);
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
lock (_listenersLock)
|
||||
_listeners.Remove(subscription);
|
||||
|
||||
subscription.Status = SubscriptionStatus.Closed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -989,7 +996,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
List<Subscription> subList;
|
||||
lock (_listenersLock)
|
||||
subList = _listeners.OfType<Subscription>().Where(x => !x.Closed).Skip(batch * batchSize).Take(batchSize).ToList();
|
||||
subList = _listeners.OfType<Subscription>().Where(x => x.Active).Skip(batch * batchSize).Take(batchSize).ToList();
|
||||
|
||||
if (subList.Count == 0)
|
||||
break;
|
||||
@@ -998,34 +1005,32 @@ namespace CryptoExchange.Net.Sockets
|
||||
foreach (var subscription in subList)
|
||||
{
|
||||
subscription.ConnectionInvocations = 0;
|
||||
if (subscription.Closed)
|
||||
if (!subscription.Active)
|
||||
// Can be closed during resubscribing
|
||||
continue;
|
||||
|
||||
subscription.IsResubscribing = true;
|
||||
subscription.Status = SubscriptionStatus.Subscribing;
|
||||
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
||||
subscription.IsResubscribing = false;
|
||||
subscription.Status = SubscriptionStatus.Pending;
|
||||
return result;
|
||||
}
|
||||
|
||||
var subQuery = subscription.CreateSubscriptionQuery(this);
|
||||
if (subQuery == null)
|
||||
{
|
||||
subscription.IsResubscribing = false;
|
||||
subscription.Status = SubscriptionStatus.Subscribed;
|
||||
continue;
|
||||
}
|
||||
|
||||
var waitEvent = new AsyncResetEvent(false);
|
||||
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
|
||||
{
|
||||
subscription.IsResubscribing = false;
|
||||
subscription.Status = r.Result.Success ? SubscriptionStatus.Subscribed: SubscriptionStatus.Pending;
|
||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||
waitEvent.Set();
|
||||
if (r.Result.Success)
|
||||
subscription.Confirmed = true;
|
||||
return r.Result;
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -34,21 +34,33 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Is it a user subscription
|
||||
/// </summary>
|
||||
public bool UserSubscription { get; set; }
|
||||
|
||||
|
||||
private SubscriptionStatus _status;
|
||||
/// <summary>
|
||||
/// Has the subscription been confirmed
|
||||
/// Current subscription status
|
||||
/// </summary>
|
||||
public bool Confirmed { get; set; }
|
||||
public SubscriptionStatus Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
if (_status == value)
|
||||
return;
|
||||
|
||||
_status = value;
|
||||
Task.Run(() => StatusChanged?.Invoke(value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the subscription closed
|
||||
/// Whether the subscription is active
|
||||
/// </summary>
|
||||
public bool Closed { get; set; }
|
||||
public bool Active => Status != SubscriptionStatus.Closing && Status != SubscriptionStatus.Closed;
|
||||
|
||||
/// <summary>
|
||||
/// Is the subscription currently resubscribing
|
||||
/// Whether the unsubscribing of this subscription lead to the closing of the connection
|
||||
/// </summary>
|
||||
public bool IsResubscribing { get; set; }
|
||||
public bool IsClosingConnection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Logger
|
||||
@@ -74,6 +86,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// Exception event
|
||||
/// </summary>
|
||||
public event Action<Exception>? Exception;
|
||||
/// <summary>
|
||||
/// Listener unsubscribed event
|
||||
/// </summary>
|
||||
public event Action<SubscriptionStatus>? StatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Subscription topic
|
||||
@@ -163,7 +179,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
Confirmed = false;
|
||||
Status = SubscriptionStatus.Pending;
|
||||
DoHandleReset();
|
||||
}
|
||||
|
||||
@@ -185,12 +201,12 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// State of this subscription
|
||||
/// </summary>
|
||||
/// <param name="Id">The id of the subscription</param>
|
||||
/// <param name="Confirmed">True when the subscription query is handled (either accepted or rejected)</param>
|
||||
/// <param name="Status">Subscription status</param>
|
||||
/// <param name="Invocations">Number of times this subscription got a message</param>
|
||||
/// <param name="ListenMatcher">Matcher for this subscription</param>
|
||||
public record SubscriptionState(
|
||||
int Id,
|
||||
bool Confirmed,
|
||||
SubscriptionStatus Status,
|
||||
int Invocations,
|
||||
MessageMatcher ListenMatcher
|
||||
);
|
||||
@@ -201,7 +217,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public SubscriptionState GetState()
|
||||
{
|
||||
return new SubscriptionState(Id, Confirmed, TotalInvocations, MessageMatcher);
|
||||
return new SubscriptionState(Id, Status, TotalInvocations, MessageMatcher);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <param name="authenticated"></param>
|
||||
public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false)
|
||||
{
|
||||
Confirmed = true;
|
||||
Status = SubscriptionStatus.Subscribed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -415,7 +415,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
var value = jsonValue.GetDecimal();
|
||||
if (objectValue is DateTime time)
|
||||
{
|
||||
if (time != DateTimeConverter.ParseFromDouble((double)value))
|
||||
if (time != DateTimeConverter.ParseFromDecimal(value))
|
||||
throw new Exception($"{method}: {property} not equal: {DateTimeConverter.ParseFromDouble((double)value!)} vs {time}");
|
||||
}
|
||||
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
||||
|
||||
@@ -325,9 +325,12 @@ namespace CryptoExchange.Net.Trackers.Trades
|
||||
if (Period != null)
|
||||
items = items.Where(e => e.Timestamp >= DateTime.UtcNow.Add(-Period.Value));
|
||||
|
||||
_snapshotId = data.Max(d => d.Timestamp.Ticks);
|
||||
foreach (var item in items.OrderBy(d => d.Timestamp))
|
||||
_data.Add(item);
|
||||
if (items.Any())
|
||||
{
|
||||
_snapshotId = data.Max(d => d.Timestamp.Ticks);
|
||||
foreach (var item in items.OrderBy(d => d.Timestamp))
|
||||
_data.Add(item);
|
||||
}
|
||||
|
||||
_snapshotSet = true;
|
||||
_changed = true;
|
||||
|
||||
@@ -5,29 +5,32 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="11.3.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="9.3.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="2.4.1" />
|
||||
<PackageReference Include="Bybit.Net" Version="5.4.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="9.3.0" />
|
||||
<PackageReference Include="CoinW.Net" Version="1.0.1" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="2.4.0" />
|
||||
<PackageReference Include="DeepCoin.Net" Version="2.3.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="2.4.0" />
|
||||
<PackageReference Include="HyperLiquid.Net" Version="2.4.0" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="2.3.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="2.3.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="3.3.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="3.3.1" />
|
||||
<PackageReference Include="JKorf.BitMEX.Net" Version="2.3.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="2.3.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="7.3.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="6.3.1" />
|
||||
<PackageReference Include="Kucoin.Net" Version="7.3.0" />
|
||||
<PackageReference Include="Binance.Net" Version="11.9.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="9.9.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="2.10.0" />
|
||||
<PackageReference Include="BloFin.Net" Version="1.2.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="5.10.1" />
|
||||
<PackageReference Include="CoinEx.Net" Version="9.9.0" />
|
||||
<PackageReference Include="CoinW.Net" Version="1.6.0" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="2.10.0" />
|
||||
<PackageReference Include="DeepCoin.Net" Version="2.9.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="2.11.0" />
|
||||
<PackageReference Include="HyperLiquid.Net" Version="2.14.0" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="2.9.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="2.9.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="3.10.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="3.9.0" />
|
||||
<PackageReference Include="Jkorf.Aster.Net" Version="1.1.0" />
|
||||
<PackageReference Include="JKorf.BitMEX.Net" Version="2.9.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="2.9.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="7.9.0" />
|
||||
<PackageReference Include="JKorf.Upbit.Net" Version="1.0.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="6.9.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="7.9.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="Toobit.Net" Version="1.2.1" />
|
||||
<PackageReference Include="WhiteBit.Net" Version="2.4.0" />
|
||||
<PackageReference Include="XT.Net" Version="2.3.1" />
|
||||
<PackageReference Include="Toobit.Net" Version="1.8.0" />
|
||||
<PackageReference Include="WhiteBit.Net" Version="2.10.0" />
|
||||
<PackageReference Include="XT.Net" Version="2.9.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
@page "/"
|
||||
@inject IAsterRestClient asterClient
|
||||
@inject IBinanceRestClient binanceClient
|
||||
@inject IBingXRestClient bingXClient
|
||||
@inject IBitfinexRestClient bitfinexClient
|
||||
@inject IBitgetRestClient bitgetClient
|
||||
@inject IBitMartRestClient bitmartClient
|
||||
@inject IBitMEXRestClient bitmexClient
|
||||
@inject IBloFinRestClient bloFinClient
|
||||
@inject IBybitRestClient bybitClient
|
||||
@inject ICoinbaseRestClient coinbaseClient
|
||||
@inject ICoinExRestClient coinexClient
|
||||
@@ -19,6 +21,7 @@
|
||||
@inject IMexcRestClient mexcClient
|
||||
@inject IOKXRestClient okxClient
|
||||
@inject IToobitRestClient toobitClient
|
||||
@inject IUpbitRestClient upbitClient
|
||||
@inject IWhiteBitRestClient whitebitClient
|
||||
@inject IXTRestClient xtClient
|
||||
|
||||
@@ -33,12 +36,14 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var asterTask = asterClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT");
|
||||
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
||||
var bitgetTask = bitgetClient.SpotApiV2.ExchangeData.GetTickersAsync("BTCUSDT");
|
||||
var bitmartTask = bitmartClient.SpotApi.ExchangeData.GetTickerAsync("BTC_USDT");
|
||||
var bitmexTask = bitmexClient.ExchangeApi.ExchangeData.GetSymbolsAsync("XBT_USDT");
|
||||
var bloFinTask = bloFinClient.FuturesApi.ExchangeData.GetTickersAsync("BTC-USDT");
|
||||
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
|
||||
var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");
|
||||
var coinexTask = coinexClient.SpotApiV2.ExchangeData.GetTickersAsync(["BTCUSDT"]);
|
||||
@@ -47,16 +52,20 @@
|
||||
var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot);
|
||||
var gateioTask = gateioClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT");
|
||||
var htxTask = htxClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
|
||||
var hyperLiquidTask = hyperLiquidClient.FuturesApi.ExchangeData.GetExchangeInfoAndTickersAsync(); // HyperLiquid does not have BTC spot trading
|
||||
var hyperLiquidTask = hyperLiquidClient.FuturesApi.ExchangeData.GetExchangeInfoAndTickersAsync();
|
||||
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
|
||||
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
var toobitTask = toobitClient.SpotApi.ExchangeData.GetTickersAsync("BTCUSDT");
|
||||
var upbitTask = upbitClient.SpotApi.ExchangeData.GetTickerAsync("USDT-BTC");
|
||||
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
|
||||
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
|
||||
|
||||
await Task.WhenAll(binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bybitTask, coinexTask, deepCoinTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask);
|
||||
await Task.WhenAll(asterTask, binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bloFinTask, bitmexTask, bybitTask, coinexTask, coinWTask, deepCoinTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask);
|
||||
|
||||
if (asterTask.Result.Success)
|
||||
_prices.Add("Aster", asterTask.Result.Data.LastPrice);
|
||||
|
||||
if (binanceTask.Result.Success)
|
||||
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
|
||||
@@ -76,6 +85,9 @@
|
||||
if (bitmexTask.Result.Success)
|
||||
_prices.Add("BitMEX", bitmexTask.Result.Data.First().LastPrice);
|
||||
|
||||
if (bloFinTask.Result.Success)
|
||||
_prices.Add("BloFin", bloFinTask.Result.Data.First().LastPrice);
|
||||
|
||||
if (bybitTask.Result.Success)
|
||||
_prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);
|
||||
|
||||
@@ -126,6 +138,9 @@
|
||||
if (toobitTask.Result.Success)
|
||||
_prices.Add("Toobit", toobitTask.Result.Data.Single().LastPrice ?? 0);
|
||||
|
||||
if (upbitTask.Result.Success)
|
||||
_prices.Add("Upbit", upbitTask.Result.Data.LastPrice);
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
@page "/LiveData"
|
||||
@inject IAsterSocketClient asterSocketClient
|
||||
@inject IBinanceSocketClient binanceSocketClient
|
||||
@inject IBingXSocketClient bingXSocketClient
|
||||
@inject IBitfinexSocketClient bitfinexSocketClient
|
||||
@inject IBitgetSocketClient bitgetSocketClient
|
||||
@inject IBitMartSocketClient bitmartSocketClient
|
||||
@inject IBitMEXSocketClient bitmexSocketClient
|
||||
@inject IBloFinSocketClient bloFinSocketClient
|
||||
@inject IBybitSocketClient bybitSocketClient
|
||||
@inject ICoinbaseSocketClient coinbaseSocketClient
|
||||
@inject ICoinExSocketClient coinExSocketClient
|
||||
@@ -19,6 +21,7 @@
|
||||
@inject IMexcSocketClient mexcSocketClient
|
||||
@inject IOKXSocketClient okxSocketClient
|
||||
@inject IToobitSocketClient toobitSocketClient
|
||||
@inject IUpbitSocketClient upbitSocketClient
|
||||
@inject IWhiteBitSocketClient whitebitSocketClient
|
||||
@inject IXTSocketClient xtSocketClient
|
||||
@using System.Collections.Concurrent
|
||||
@@ -42,12 +45,16 @@
|
||||
{
|
||||
var tasks = new Task<CallResult<UpdateSubscription>>[]
|
||||
{
|
||||
// Aster doesn't support the ETH/BTC pair
|
||||
//asterSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Aster", data.Data.LastPrice)),
|
||||
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
||||
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
|
||||
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
||||
bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
|
||||
bitmartSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("BitMart", data.Data.LastPrice)),
|
||||
bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)),
|
||||
// BloFin doesn't support the ETH/BTC pair
|
||||
//bloFinSocketClient.FuturesApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BloFin", data.Data.LastPrice)),
|
||||
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
||||
coinExSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync(["ETHBTC"], data => UpdateData("CoinEx", data.Data.First().LastPrice)),
|
||||
coinWSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CoinW", data.Data.LastPrice)),
|
||||
@@ -61,10 +68,12 @@
|
||||
//hyperLiquidSocketClient.SpotApi.SubscribeToSymbolUpdatesAsync("ETH", data => UpdateData("HyperLiquid", data.Data.MidPrice ?? 0)),
|
||||
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("Kraken", data.Data.LastPrice)),
|
||||
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
||||
mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
|
||||
// Mexc doesn't offer a ticker stream currently
|
||||
//mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
|
||||
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
|
||||
// Toobit doesn't support the ETH/BTC pair
|
||||
//toobitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Toobit", data.Data.LastPrice ?? 0)),
|
||||
upbitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("BTC-ETH", data => UpdateData("Upbit", data.Data.LastPrice)),
|
||||
whitebitSocketClient.V4Api.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("WhiteBit", data.Data.Ticker.LastPrice)),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
@page "/OrderBooks"
|
||||
@using System.Collections.Concurrent
|
||||
@using System.Timers
|
||||
@using Aster.Net.Interfaces
|
||||
@using Binance.Net.Interfaces
|
||||
@using BingX.Net.Interfaces
|
||||
@using Bitfinex.Net.Interfaces
|
||||
@using Bitget.Net.Interfaces;
|
||||
@using BitMart.Net.Interfaces;
|
||||
@using BitMEX.Net.Interfaces;
|
||||
@using BloFin.Net.Interfaces;
|
||||
@using Bybit.Net.Interfaces
|
||||
@using CoinEx.Net.Interfaces
|
||||
@using CoinW.Net.Interfaces
|
||||
@@ -23,15 +25,18 @@
|
||||
@using Kucoin.Net.Interfaces
|
||||
@using Mexc.Net.Interfaces
|
||||
@using OKX.Net.Interfaces;
|
||||
@using Upbit.Net.Interfaces;
|
||||
@using Toobit.Net.Interfaces;
|
||||
@using WhiteBit.Net.Interfaces
|
||||
@using XT.Net.Interfaces
|
||||
@inject IAsterOrderBookFactory asterFactory
|
||||
@inject IBinanceOrderBookFactory binanceFactory
|
||||
@inject IBingXOrderBookFactory bingXFactory
|
||||
@inject IBitfinexOrderBookFactory bitfinexFactory
|
||||
@inject IBitgetOrderBookFactory bitgetFactory
|
||||
@inject IBitMartOrderBookFactory bitmartFactory
|
||||
@inject IBitMEXOrderBookFactory bitmexFactory
|
||||
@inject IBloFinOrderBookFactory bloFinFactory
|
||||
@inject IBybitOrderBookFactory bybitFactory
|
||||
@inject ICoinbaseOrderBookFactory coinbaseFactory
|
||||
@inject ICoinExOrderBookFactory coinExFactory
|
||||
@@ -46,6 +51,7 @@
|
||||
@inject IMexcOrderBookFactory mexcFactory
|
||||
@inject IOKXOrderBookFactory okxFactory
|
||||
@inject IToobitOrderBookFactory toobitFactory
|
||||
@inject IUpbitOrderBookFactory upbitFactory
|
||||
@inject IWhiteBitOrderBookFactory whitebitFactory
|
||||
@inject IXTOrderBookFactory xtFactory
|
||||
@implements IDisposable
|
||||
@@ -55,7 +61,7 @@
|
||||
@foreach(var book in _books.OrderBy(p => p.Key))
|
||||
{
|
||||
<div style="margin-bottom: 20px; flex: 1; min-width: 300px;">
|
||||
<h4>@book.Key</h4>
|
||||
<h4>@book.Key - @book.Value.Symbol</h4>
|
||||
@if (book.Value.AskCount >= 3 && book.Value.BidCount >= 3)
|
||||
{
|
||||
for (var i = 0; i < 3; i++)
|
||||
@@ -81,29 +87,29 @@
|
||||
|
||||
_books = new Dictionary<string, ISymbolOrderBook>
|
||||
{
|
||||
{ "Aster", binanceFactory.CreateSpot("ETHUSDT") },
|
||||
{ "Binance", binanceFactory.CreateSpot("ETHBTC") },
|
||||
{ "BingX", bingXFactory.CreateSpot("ETH-BTC") },
|
||||
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
|
||||
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
|
||||
{ "BitMart", bitmartFactory.CreateSpot("ETH_BTC", null) },
|
||||
{ "BitMEX", bitmexFactory.Create("ETH_XBT") },
|
||||
{ "BloFin", bloFinFactory.CreateFutures("ETH-USDT") },
|
||||
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
|
||||
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
|
||||
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
||||
{ "CoinW", coinWFactory.CreateSpot("ETH_BTC") },
|
||||
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
|
||||
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
|
||||
// DeepCoin does not support the ETH/BTC pair
|
||||
//{ "DeepCoin", deepCoinFactory.Create("ETH-BTC") },
|
||||
{ "DeepCoin", deepCoinFactory.Create("ETH-USDT") },
|
||||
{ "HTX", htxFactory.CreateSpot("ethbtc") },
|
||||
// HyperLiquid does not support the ETH/BTC pair
|
||||
//{ "HyperLiquid", hyperLiquidFactory.Create("ETH/BTC") },
|
||||
{ "HyperLiquid", hyperLiquidFactory.Create("UETH/USDC") },
|
||||
{ "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
|
||||
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
|
||||
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") },
|
||||
{ "OKX", okxFactory.Create("ETH-BTC") },
|
||||
// Toobit does not support the ETH/BTC pair
|
||||
//{ "Toobit", toobitFactory.Create("ETH/BTC") },
|
||||
{ "Toobit", toobitFactory.CreateSpot("ETHUSDT") },
|
||||
{ "Upbit", upbitFactory.CreateSpot("BTC-ETH") },
|
||||
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
|
||||
{ "XT", xtFactory.CreateSpot("eth_btc") },
|
||||
};
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
@page "/Trackers"
|
||||
@using System.Collections.Concurrent
|
||||
@using System.Timers
|
||||
@using Aster.Net.Interfaces
|
||||
@using Binance.Net.Interfaces
|
||||
@using BingX.Net.Interfaces
|
||||
@using Bitfinex.Net.Interfaces
|
||||
@using Bitget.Net.Interfaces;
|
||||
@using BitMEX.Net.Interfaces;
|
||||
@using BitMart.Net.Interfaces;
|
||||
@using BloFin.Net.Interfaces
|
||||
@using Bybit.Net.Interfaces
|
||||
@using CoinEx.Net.Interfaces
|
||||
@using CoinW.Net.Interfaces
|
||||
@@ -24,15 +26,18 @@
|
||||
@using Kucoin.Net.Interfaces
|
||||
@using Mexc.Net.Interfaces
|
||||
@using OKX.Net.Interfaces;
|
||||
@using Upbit.Net.Interfaces;
|
||||
@using Toobit.Net.Interfaces;
|
||||
@using WhiteBit.Net.Interfaces
|
||||
@using XT.Net.Interfaces
|
||||
@inject IAsterTrackerFactory asterFactory
|
||||
@inject IBinanceTrackerFactory binanceFactory
|
||||
@inject IBingXTrackerFactory bingXFactory
|
||||
@inject IBitfinexTrackerFactory bitfinexFactory
|
||||
@inject IBitgetTrackerFactory bitgetFactory
|
||||
@inject IBitMartTrackerFactory bitmartFactory
|
||||
@inject IBitMEXTrackerFactory bitmexFactory
|
||||
@inject IBloFinTrackerFactory bloFinFactory
|
||||
@inject IBybitTrackerFactory bybitFactory
|
||||
@inject ICoinbaseTrackerFactory coinbaseFactory
|
||||
@inject ICoinExTrackerFactory coinExFactory
|
||||
@@ -47,11 +52,12 @@
|
||||
@inject IMexcTrackerFactory mexcFactory
|
||||
@inject IOKXTrackerFactory okxFactory
|
||||
@inject IToobitTrackerFactory toobitFactory
|
||||
@inject IUpbitTrackerFactory upbitFactory
|
||||
@inject IWhiteBitTrackerFactory whitebitFactory
|
||||
@inject IXTTrackerFactory xtFactory
|
||||
@implements IDisposable
|
||||
|
||||
<h3>ETH-BTC trade Trackers, live updates:</h3>
|
||||
<h3>Trade Trackers, live updates:</h3>
|
||||
<div style="display:flex; flex-wrap: wrap;">
|
||||
@foreach (var tracker in _trackers.OrderBy(p => p.Exchange))
|
||||
{
|
||||
@@ -71,36 +77,40 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var usdtSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var usdcSpotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDC");
|
||||
var usdtSpotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var usdtPerpLinSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
||||
|
||||
_trackers = new List<ITradeTracker>
|
||||
{
|
||||
{ binanceFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bingXFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitfinexFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitgetFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitmartFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitmexFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bybitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinbaseFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinExFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinWFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ cryptocomFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ deepCoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ gateioFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ htxFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
// HyperLiquid doesn't support spot pair, but does have a futures BTC/USDC pair
|
||||
{ hyperLiquidFactory.CreateTradeTracker(new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDC"), period: TimeSpan.FromMinutes(5)) },
|
||||
{ krakenFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ kucoinFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ mexcFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ okxFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ toobitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ whitebitFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ xtFactory.CreateTradeTracker(usdtSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ asterFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ binanceFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bingXFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitfinexFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitgetFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitmartFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitmexFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bloFinFactory.CreateTradeTracker(usdtPerpLinSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bybitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinbaseFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinExFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinWFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ cryptocomFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ deepCoinFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ gateioFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ htxFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ hyperLiquidFactory.CreateTradeTracker(usdcSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ krakenFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ kucoinFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ mexcFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ okxFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ toobitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ upbitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ whitebitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ xtFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
|
||||
};
|
||||
|
||||
await Task.WhenAll(_trackers.Select(b => b.StartAsync()));
|
||||
await Task.WhenAll(_trackers.Select(b => b.StartAsync(false)));
|
||||
|
||||
// Use a manual update timer so the page isn't refreshed too often
|
||||
_timer = new Timer(500);
|
||||
|
||||
@@ -33,11 +33,13 @@ namespace BlazorClient
|
||||
restOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||
});
|
||||
|
||||
services.AddAster();
|
||||
services.AddBingX();
|
||||
services.AddBitfinex();
|
||||
services.AddBitget();
|
||||
services.AddBitMart();
|
||||
services.AddBitMEX();
|
||||
services.AddBloFin();
|
||||
services.AddBybit();
|
||||
services.AddCoinbase();
|
||||
services.AddCoinEx();
|
||||
@@ -52,6 +54,7 @@ namespace BlazorClient
|
||||
services.AddMexc();
|
||||
services.AddOKX();
|
||||
services.AddToobit();
|
||||
services.AddUpbit();
|
||||
services.AddWhiteBit();
|
||||
services.AddXT();
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using BlazorClient
|
||||
@using BlazorClient.Shared
|
||||
@using Aster.Net.Interfaces.Clients;
|
||||
@using Binance.Net.Interfaces.Clients;
|
||||
@using BingX.Net.Interfaces.Clients;
|
||||
@using Bitfinex.Net.Interfaces.Clients;
|
||||
@using Bitget.Net.Interfaces.Clients;
|
||||
@using BitMart.Net.Interfaces.Clients;
|
||||
@using BitMEX.Net.Interfaces.Clients;
|
||||
@using BloFin.Net.Interfaces.Clients;
|
||||
@using Bybit.Net.Interfaces.Clients;
|
||||
@using Coinbase.Net.Interfaces.Clients;
|
||||
@using CoinEx.Net.Interfaces.Clients;
|
||||
@@ -27,6 +29,7 @@
|
||||
@using Kucoin.Net.Interfaces.Clients;
|
||||
@using Mexc.Net.Interfaces.Clients;
|
||||
@using OKX.Net.Interfaces.Clients;
|
||||
@using Upbit.Net.Interfaces.Clients;
|
||||
@using Toobit.Net.Interfaces.Clients;
|
||||
@using WhiteBit.Net.Interfaces.Clients
|
||||
@using XT.Net.Interfaces.Clients
|
||||
|
||||
@@ -6,20 +6,20 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="11.1.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="9.1.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="2.1.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="5.1.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="9.1.0" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="2.1.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="3.1.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="3.1.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="7.1.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="6.1.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="7.1.0" />
|
||||
<PackageReference Include="Binance.Net" Version="11.9.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="9.9.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="2.10.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="5.10.1" />
|
||||
<PackageReference Include="CoinEx.Net" Version="9.9.0" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="2.10.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="2.11.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="2.9.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="3.10.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="3.9.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="2.9.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="7.9.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="6.9.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="7.9.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="11.1.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="2.1.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="3.1.0" />
|
||||
<PackageReference Include="Binance.Net" Version="11.9.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="2.10.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="3.9.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -12,12 +12,14 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|
||||
|
||||
||Exchange|Type|Repository|Nuget|Referral Link|Referral Fee Discount|
|
||||
|--|--|--|--|--|--|--|
|
||||
||Aster|DEX|[JKorf/Aster.Net](https://github.com/JKorf/Aster.Net)|[](https://www.nuget.org/packages/JKorf.Aster.Net)|[Link](https://www.asterdex.com/en/referral/FD2E11)|4%|
|
||||
||Binance|CEX|[JKorf/Binance.Net](https://github.com/JKorf/Binance.Net)|[](https://www.nuget.org/packages/Binance.Net)|[Link](https://accounts.binance.com/register?ref=X5K3F2ZG)|20%|
|
||||
||BingX|CEX|[JKorf/BingX.Net](https://github.com/JKorf/BingX.Net)|[](https://www.nuget.org/packages/JK.BingX.Net)|[Link](https://bingx.com/invite/FFHRJKWG/)|20%|
|
||||
||Bitfinex|CEX|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[](https://www.nuget.org/packages/Bitfinex.Net)|-|-|
|
||||
||Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%|
|
||||
||BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%|
|
||||
||BitMEX|CEX|[JKorf/BitMEX.Net](https://github.com/JKorf/BitMEX.Net)|[](https://www.nuget.org/packages/JKorf.BitMEX.Net)|[Link](https://www.bitmex.com/app/register/94f98e)|30%|
|
||||
||BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[](https://www.nuget.org/packages/BloFin.Net)|-|-|
|
||||
||Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-|
|
||||
||Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-|
|
||||
||CoinEx|CEX|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[](https://www.nuget.org/packages/CoinEx.Net)|[Link](https://www.coinex.com/register?rc=rbtnp)|20%|
|
||||
@@ -33,11 +35,16 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|
||||
||Mexc|CEX|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[](https://www.nuget.org/packages/JK.Mexc.Net)|-|-|
|
||||
||OKX|CEX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[](https://www.nuget.org/packages/JK.OKX.Net)|[Link](https://www.okx.com/join/14592495)|20%|
|
||||
||Toobit|CEX|[JKorf/Toobit.Net](https://github.com/JKorf/Toobit.Net)|[](https://www.nuget.org/packages/Toobit.Net)|[Link](https://www.toobit.com/en-US/register?invite_code=zsV19h)|-|
|
||||
||Upbit|CEX|[JKorf/Upbit.Net](https://github.com/JKorf/Upbit.Net)|[](https://www.nuget.org/packages/JKorf.Upbit.Net)|-|-|
|
||||
||WhiteBit|CEX|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[](https://www.nuget.org/packages/WhiteBit.Net)|[Link](https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf)|-|
|
||||
||XT|CEX|[JKorf/XT.Net](https://github.com/JKorf/XT.Net)|[](https://www.nuget.org/packages/XT.Net)|[Link](https://www.xt.com/ru/accounts/register?ref=CZG39C)|25%|
|
||||
|
||||
Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's.
|
||||
|
||||
### Full demo application
|
||||
A full demo application is available using the [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) library:
|
||||
https://github.com/JKorf/CryptoManager.Net
|
||||
|
||||
## Discord
|
||||
[](https://discord.gg/MSpeEtSY8t)
|
||||
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
|
||||
@@ -59,6 +66,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 9.11.0 - 30 Oct 2025
|
||||
* Added StaticLogger to LibraryHelpers, updated warning logging for converters to use StaticLogger
|
||||
* Added client reference helper to LibraryHelpers
|
||||
* Fixed exception when initial trade snapshot has no items in TradeTracker
|
||||
|
||||
* Version 9.10.0 - 15 Oct 2025
|
||||
* Added ITransferRestClient Shared interface
|
||||
* Added ClientOrderId property to SharedUserTrade model
|
||||
* Updated IBalanceRestClient, GetBalancesRequest now mainly works with SharedAccountType type, allowing more options
|
||||
* Updated IBalanceRestClient, GetBalanceOptions now specifies supported account types
|
||||
* Updated DateTimeConverter to work primarily with decimal instead of double to prevent some floating point issues
|
||||
|
||||
* Version 9.9.0 - 06 Oct 2025
|
||||
* Updated socket Subscription status handling
|
||||
* Added SubscriptionStatusChanged event to UpdateSubscription (SubscribeAsync methods reponse)
|
||||
* Fixed timing issue for connection events in UpdateSubscription
|
||||
|
||||
* Version 9.8.0 - 30 Sep 2025
|
||||
* Added ContractAddress to SharedAsset model
|
||||
* Added ITrackerFactory interface
|
||||
* Fixed UpdateSubscription still propagating connection events even though the specific listener is unsubscribed
|
||||
* Fixed ExchangeHelpers.AdjustValueStep high precision calculation
|
||||
* Fixed issue increasing the number of websocket connections increasing when sending a query when a previous connection was attempting to reconnect
|
||||
* Fixed EnumConverter to allow mapping empty string values
|
||||
|
||||
* Version 9.7.0 - 01 Sep 2025
|
||||
* Added LibraryHelpers.CreateHttpClientMessageHandle to standardize HttpMessageHandler creation
|
||||
* Added REST client option for selecting HTTP protocol version
|
||||
|
||||
Reference in New Issue
Block a user