1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-10-27 16:37:24 +00:00

Compare commits

...

16 Commits

Author SHA1 Message Date
Jkorf
dd60067684 Updated examples 2025-10-27 12:00:51 +01:00
Jkorf
04e4ddf525 Fixed exception when initial trade snapshot has no items in TradeTracker 2025-10-27 11:59:54 +01:00
Jkorf
99bf6d7c75 Added Upbit reference 2025-10-27 11:44:12 +01:00
Jkorf
99a203933c Added missing release notes 2025-10-15 13:59:10 +02:00
Jkorf
b43d2a2040 Updated to version 9.10.0 2025-10-15 13:36:33 +02:00
Jkorf
ba9c406def Updated CryptoExchange.Net version 2025-10-15 13:34:50 +02:00
Jkorf
f5f4d50cc9 Updated to version 9.10.0 2025-10-15 13:24:25 +02:00
Jkorf
f87506b490 Added ITransferRestClient, updated Shared IBalanceRestClient to use SharedAccountType 2025-10-15 13:21:00 +02:00
Jkorf
f6f9a53ce5 Added ClientOrderId property to SharedUserTrade model 2025-10-13 15:42:27 +02:00
Jkorf
61130ef54e Added long overloads for parse methods in DateTimeConverter 2025-10-13 11:15:58 +02:00
Jkorf
e8bcbd59be Updated DateTimeConverter to work primarily with decimal values instead of doubles to fix some floating point parsing issues 2025-10-13 09:06:14 +02:00
Jkorf
d433ff7475 Updated to version 9.9.0 2025-10-06 13:47:21 +02:00
Jkorf
71957037d0 Updated CryptoExchange.Net version to 9.8.0 2025-10-06 13:45:47 +02:00
Jkorf
bcdcdbbd4e Updated to version 9.9.0 2025-10-06 13:26:23 +02:00
Jkorf
1ece13f5bc Updated socket Subscription status handling, fixing timing issue for connection events and adding SubscriptionStatusChanged event 2025-10-06 13:22:40 +02:00
Jkorf
da70ba6ec7 Added Aster reference 2025-10-06 10:37:28 +02:00
34 changed files with 666 additions and 171 deletions

View File

@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net.Protobuf</PackageId> <PackageId>CryptoExchange.Net.Protobuf</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description> <Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>9.8.0</PackageVersion> <PackageVersion>9.10.0</PackageVersion>
<AssemblyVersion>9.8.0</AssemblyVersion> <AssemblyVersion>9.10.0</AssemblyVersion>
<FileVersion>9.8.0</FileVersion> <FileVersion>9.10.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags> <PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@ -41,7 +41,7 @@
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile> <DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CryptoExchange.Net" Version="9.8.0" /> <PackageReference Include="CryptoExchange.Net" Version="9.10.0" />
<PackageReference Include="protobuf-net" Version="3.2.56" /> <PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -5,6 +5,12 @@
Protobuf support for CryptoExchange.Net. Protobuf support for CryptoExchange.Net.
## Release notes ## Release notes
* 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 * Version 9.8.0 - 30 Sep 2025
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/ * Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/

View File

@ -6,10 +6,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.3.2"></PackageReference> <PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"></PackageReference> <PackageReference Include="NUnit3TestAdapter" Version="5.2.0"></PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -202,7 +202,7 @@ namespace CryptoExchange.Net.UnitTests
await sub; await sub;
// assert // assert
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed); ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
} }
[TestCase()] [TestCase()]
@ -225,7 +225,7 @@ namespace CryptoExchange.Net.UnitTests
await sub; await sub;
// assert // assert
Assert.That(client.SubClient.TestSubscription.Confirmed); Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
} }
} }
} }

View File

@ -269,6 +269,7 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
} }
subscription.Status = SubscriptionStatus.Subscribing;
var waitEvent = new AsyncResetEvent(false); var waitEvent = new AsyncResetEvent(false);
var subQuery = subscription.CreateSubscriptionQuery(socketConnection); var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
if (subQuery != null) if (subQuery != null)
@ -279,7 +280,7 @@ namespace CryptoExchange.Net.Clients
{ {
waitEvent?.Set(); waitEvent?.Set();
var isTimeout = subResult.Error is CancellationRequestedError; 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 // 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()); _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 // 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); await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!); return new CallResult<UpdateSubscription>(subResult.Error!);
} }
@ -295,7 +297,7 @@ namespace CryptoExchange.Net.Clients
subscription.HandleSubQueryResponse(subQuery.Response!); subscription.HandleSubQueryResponse(subQuery.Response!);
} }
subscription.Confirmed = true; subscription.Status = SubscriptionStatus.Subscribed;
if (ct != default) if (ct != default)
{ {
subscription.CancellationTokenRegistration = ct.Register(async () => subscription.CancellationTokenRegistration = ct.Register(async () =>
@ -847,7 +849,7 @@ namespace CryptoExchange.Net.Clients
cs.SubscriptionStates.ForEach(subState => cs.SubscriptionStates.ForEach(subState =>
{ {
sb.AppendLine($"\t\t\tId: {subState.Id}"); 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\tInvocations: {subState.Invocations}");
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]"); sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
}); });

View File

@ -14,8 +14,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000; private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d; private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000; private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
/// <inheritdoc /> /// <inheritdoc />
public override bool CanConvert(Type typeToConvert) public override bool CanConvert(Type typeToConvert)
@ -45,11 +45,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType is JsonTokenType.Number) if (reader.TokenType is JsonTokenType.Number)
{ {
var longValue = reader.GetDouble(); var decValue = reader.GetDecimal();
if (longValue == 0 || longValue < 0) if (decValue == 0 || decValue < 0)
return default; return default;
return ParseFromDouble(longValue); return ParseFromDecimal(decValue);
} }
else if (reader.TokenType is JsonTokenType.String) else if (reader.TokenType is JsonTokenType.String)
{ {
@ -57,7 +57,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (string.IsNullOrWhiteSpace(stringValue) if (string.IsNullOrWhiteSpace(stringValue)
|| stringValue == "-1" || stringValue == "-1"
|| stringValue == "0001-01-01T00:00:00Z" || stringValue == "0001-01-01T00:00:00Z"
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0) || decimal.TryParse(stringValue, out var decVal) && decVal == 0)
{ {
return default; return default;
} }
@ -88,20 +88,24 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
/// <summary> /// <summary>
/// Parse a long value to datetime /// Parse a double value to datetime
/// </summary> /// </summary>
/// <param name="longValue"></param> public static DateTime ParseFromDouble(double value)
/// <returns></returns> => ParseFromDecimal((decimal)value);
public static DateTime ParseFromDouble(double longValue)
{
if (longValue < 19999999999)
return ConvertFromSeconds(longValue);
if (longValue < 19999999999999)
return ConvertFromMilliseconds(longValue);
if (longValue < 19999999999999999)
return ConvertFromMicroseconds(longValue);
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> /// <summary>
@ -152,19 +156,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc); 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 // Parse 1637745563.000 format
if (doubleValue <= 0) if (decimalValue <= 0)
return default; return default;
if (doubleValue < 19999999999) if (decimalValue < 19999999999)
return ConvertFromSeconds(doubleValue); return ConvertFromSeconds(decimalValue);
if (doubleValue < 19999999999999) if (decimalValue < 19999999999999)
return ConvertFromMilliseconds((long)doubleValue); return ConvertFromMilliseconds(decimalValue);
if (doubleValue < 19999999999999999) if (decimalValue < 19999999999999999)
return ConvertFromMicroseconds((long)doubleValue); return ConvertFromMicroseconds(decimalValue);
return ConvertFromNanoseconds((long)doubleValue); return ConvertFromNanoseconds(decimalValue);
} }
if (stringValue.Length == 10) if (stringValue.Length == 10)
@ -188,54 +192,70 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <summary> /// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime /// Convert a seconds since epoch (01-01-1970) value to DateTime
/// </summary> /// </summary>
/// <param name="seconds"></param> public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
/// <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));
/// <summary> /// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime /// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary> /// </summary>
/// <param name="nanoseconds"></param> public static DateTime ConvertFromSeconds(double seconds) => ConvertFromSeconds((decimal)seconds);
/// <returns></returns> /// <summary>
public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond)); /// 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> /// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value /// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary> /// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")] [return: NotNullIfNotNull("time")]
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds); public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
/// <summary> /// <summary>
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value /// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
/// </summary> /// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")] [return: NotNullIfNotNull("time")]
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds); public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
/// <summary> /// <summary>
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value /// Convert a DateTime value to microseconds since epoch (01-01-1970) value
/// </summary> /// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")] [return: NotNullIfNotNull("time")]
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond); public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
/// <summary> /// <summary>
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value /// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
/// </summary> /// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")] [return: NotNullIfNotNull("time")]
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond); public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
} }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
</PropertyGroup> </PropertyGroup>
@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>9.8.0</PackageVersion> <PackageVersion>9.10.0</PackageVersion>
<AssemblyVersion>9.8.0</AssemblyVersion> <AssemblyVersion>9.10.0</AssemblyVersion>
<FileVersion>9.8.0</FileVersion> <FileVersion>9.10.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags> <PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@ -51,11 +51,11 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageReference Include="System.Text.Json" Version="9.0.6" /> <PackageReference Include="System.Text.Json" Version="9.0.10" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Client Packages"> <ItemGroup Label="Transitive Client Packages">
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -399,7 +399,7 @@ namespace CryptoExchange.Net
/// <summary> /// <summary>
/// Whether the trading mode is linear /// Whether the trading mode is linear
/// </summary> /// </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> /// <summary>
/// Whether the trading mode is inverse /// Whether the trading mode is inverse
@ -416,6 +416,36 @@ namespace CryptoExchange.Net
/// </summary> /// </summary>
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear; 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> /// <summary>
/// Register rest client interfaces /// Register rest client interfaces
/// </summary> /// </summary>
@ -445,6 +475,8 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IFeeRestClient)client(x)!); services.AddTransient(x => (IFeeRestClient)client(x)!);
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T))) if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IBookTickerRestClient)client(x)!); services.AddTransient(x => (IBookTickerRestClient)client(x)!);
if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITransferRestClient)client(x)!);
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T))) if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderRestClient)client(x)!); services.AddTransient(x => (ISpotOrderRestClient)client(x)!);

View File

@ -267,4 +267,31 @@ namespace CryptoExchange.Net.Objects
Succeed 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
}
} }

View File

@ -15,6 +15,7 @@ namespace CryptoExchange.Net.Objects.Sockets
private readonly Subscription _listener; private readonly Subscription _listener;
private object _eventLock = new object(); private object _eventLock = new object();
private bool _connectionEventsSubscribed = true;
private List<Action> _connectionClosedEventHandlers = new List<Action>(); private List<Action> _connectionClosedEventHandlers = new List<Action>();
private List<Action> _connectionLostEventHandlers = new List<Action>(); private List<Action> _connectionLostEventHandlers = new List<Action>();
private List<Action<Error>> _resubscribeFailedEventHandlers = new List<Action<Error>>(); private List<Action<Error>> _resubscribeFailedEventHandlers = new List<Action<Error>>();
@ -22,6 +23,11 @@ namespace CryptoExchange.Net.Objects.Sockets
private List<Action> _activityPausedEventHandlers = new List<Action>(); private List<Action> _activityPausedEventHandlers = new List<Action>();
private List<Action> _activityUnpausedEventHandlers = 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> /// <summary>
/// Event when the connection is lost. The socket will automatically reconnect when possible. /// Event when the connection is lost. The socket will automatically reconnect when possible.
/// </summary> /// </summary>
@ -113,21 +119,34 @@ namespace CryptoExchange.Net.Objects.Sockets
_connection.ActivityUnpaused += HandleUnpausedEvent; _connection.ActivityUnpaused += HandleUnpausedEvent;
_listener = subscription; _listener = subscription;
_listener.Unsubscribed += HandleUnsubscribed; _listener.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
} }
private void HandleUnsubscribed() private void UnsubscribeConnectionEvents()
{ {
_connection.ConnectionClosed -= HandleConnectionClosedEvent; lock (_eventLock)
_connection.ConnectionLost -= HandleConnectionLostEvent; {
_connection.ConnectionRestored -= HandleConnectionRestoredEvent; if (!_connectionEventsSubscribed)
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent; return;
_connection.ActivityPaused -= HandlePausedEvent;
_connection.ActivityUnpaused -= HandleUnpausedEvent; _connection.ConnectionClosed -= HandleConnectionClosedEvent;
_connection.ConnectionLost -= HandleConnectionLostEvent;
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
_connection.ActivityPaused -= HandlePausedEvent;
_connection.ActivityUnpaused -= HandleUnpausedEvent;
_connectionEventsSubscribed = false;
}
} }
private void HandleConnectionClosedEvent() private void HandleConnectionClosedEvent()
{ {
UnsubscribeConnectionEvents();
// If we're not the subscription closing this connection don't bother emitting
if (!_listener.IsClosingConnection)
return;
List<Action> handlers; List<Action> handlers;
lock (_eventLock) lock (_eventLock)
handlers = _connectionClosedEventHandlers.ToList(); handlers = _connectionClosedEventHandlers.ToList();
@ -138,6 +157,12 @@ namespace CryptoExchange.Net.Objects.Sockets
private void HandleConnectionLostEvent() private void HandleConnectionLostEvent()
{ {
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action> handlers; List<Action> handlers;
lock (_eventLock) lock (_eventLock)
handlers = _connectionLostEventHandlers.ToList(); handlers = _connectionLostEventHandlers.ToList();
@ -148,6 +173,12 @@ namespace CryptoExchange.Net.Objects.Sockets
private void HandleConnectionRestoredEvent(TimeSpan period) private void HandleConnectionRestoredEvent(TimeSpan period)
{ {
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action<TimeSpan>> handlers; List<Action<TimeSpan>> handlers;
lock (_eventLock) lock (_eventLock)
handlers = _connectionRestoredEventHandlers.ToList(); handlers = _connectionRestoredEventHandlers.ToList();
@ -158,6 +189,12 @@ namespace CryptoExchange.Net.Objects.Sockets
private void HandleResubscribeFailedEvent(Error error) private void HandleResubscribeFailedEvent(Error error)
{ {
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action<Error>> handlers; List<Action<Error>> handlers;
lock (_eventLock) lock (_eventLock)
handlers = _resubscribeFailedEventHandlers.ToList(); handlers = _resubscribeFailedEventHandlers.ToList();
@ -168,6 +205,12 @@ namespace CryptoExchange.Net.Objects.Sockets
private void HandlePausedEvent() private void HandlePausedEvent()
{ {
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action> handlers; List<Action> handlers;
lock (_eventLock) lock (_eventLock)
handlers = _activityPausedEventHandlers.ToList(); handlers = _activityPausedEventHandlers.ToList();
@ -178,6 +221,12 @@ namespace CryptoExchange.Net.Objects.Sockets
private void HandleUnpausedEvent() private void HandleUnpausedEvent()
{ {
if (!_listener.Active)
{
UnsubscribeConnectionEvents();
return;
}
List<Action> handlers; List<Action> handlers;
lock (_eventLock) lock (_eventLock)
handlers = _activityUnpausedEventHandlers.ToList(); handlers = _activityUnpausedEventHandlers.ToList();

View File

@ -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
}
}

View File

@ -12,7 +12,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Balances request options /// Balances request options
/// </summary> /// </summary>
EndpointOptions<GetBalancesRequest> GetBalancesOptions { get; } GetBalancesOptions GetBalancesOptions { get; }
/// <summary> /// <summary>
/// Get balances for the user /// Get balances for the user

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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);
}
}
}

View File

@ -8,18 +8,28 @@ namespace CryptoExchange.Net.SharedApis
public record GetBalancesRequest : SharedRequest public record GetBalancesRequest : SharedRequest
{ {
/// <summary> /// <summary>
/// Trading mode /// Account type
/// </summary> /// </summary>
public TradingMode? TradingMode { get; set; } public SharedAccountType? AccountType { get; set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="tradingMode">Trading mode</param> /// <param name="tradingMode">Trading mode</param>
/// <param name="exchangeParameters">Exchange specific parameters</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;
} }
} }
} }

View File

@ -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;
}
}
}

View File

@ -17,6 +17,7 @@
/// Total quantity /// Total quantity
/// </summary> /// </summary>
public decimal Total { get; set; } public decimal Total { get; set; }
/// <summary> /// <summary>
/// Isolated margin symbol, only applicable for isolated margin futures /// Isolated margin symbol, only applicable for isolated margin futures
/// </summary> /// </summary>

View File

@ -28,6 +28,10 @@ namespace CryptoExchange.Net.SharedApis
/// </summary> /// </summary>
public string OrderId { get; set; } public string OrderId { get; set; }
/// <summary> /// <summary>
/// The client order id
/// </summary>
public string? ClientOrderId { get; set; }
/// <summary>
/// Side of the trade /// Side of the trade
/// </summary> /// </summary>
public SharedOrderSide? Side { get; set; } public SharedOrderSide? Side { get; set; }

View File

@ -296,7 +296,7 @@ namespace CryptoExchange.Net.Sockets
lock (_listenersLock) 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(); subscription.Reset();
foreach (var query in _listeners.OfType<Query>().ToList()) foreach (var query in _listeners.OfType<Query>().ToList())
@ -527,10 +527,10 @@ namespace CryptoExchange.Net.Sockets
continue; 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 // 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 (subscriptionProcessor.SubscriptionQuery?.TimeoutBehavior == TimeoutBehavior.Succeed)
// If this subscription has a query waiting for a timeout (success if there is no error response) // 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 // 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) 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 // 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); await Task.Delay(50).ConfigureAwait(false);
subscription.Closed = true; subscription.Status = SubscriptionStatus.Closing;
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed) if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
{
subscription.Status = SubscriptionStatus.Closed;
return; return;
}
_logger.ClosingSubscription(SocketId, subscription.Id); _logger.ClosingSubscription(SocketId, subscription.Id);
if (subscription.CancellationTokenRegistration.HasValue) if (subscription.CancellationTokenRegistration.HasValue)
@ -675,7 +678,7 @@ namespace CryptoExchange.Net.Sockets
bool shouldCloseConnection; bool shouldCloseConnection;
lock (_listenersLock) 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) if (!anyDuplicateSubscription)
{ {
@ -693,6 +696,7 @@ namespace CryptoExchange.Net.Sockets
if (Status == SocketStatus.Closing) if (Status == SocketStatus.Closing)
{ {
subscription.Status = SubscriptionStatus.Closed;
_logger.AlreadyClosing(SocketId); _logger.AlreadyClosing(SocketId);
return; return;
} }
@ -700,6 +704,7 @@ namespace CryptoExchange.Net.Sockets
if (shouldCloseConnection) if (shouldCloseConnection)
{ {
Status = SocketStatus.Closing; Status = SocketStatus.Closing;
subscription.IsClosingConnection = true;
_logger.ClosingNoMoreSubscriptions(SocketId); _logger.ClosingNoMoreSubscriptions(SocketId);
await CloseAsync().ConfigureAwait(false); await CloseAsync().ConfigureAwait(false);
} }
@ -707,7 +712,7 @@ namespace CryptoExchange.Net.Sockets
lock (_listenersLock) lock (_listenersLock)
_listeners.Remove(subscription); _listeners.Remove(subscription);
subscription.InvokeUnsubscribedHandler(); subscription.Status = SubscriptionStatus.Closed;
} }
/// <summary> /// <summary>
@ -991,7 +996,7 @@ namespace CryptoExchange.Net.Sockets
List<Subscription> subList; List<Subscription> subList;
lock (_listenersLock) 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) if (subList.Count == 0)
break; break;
@ -1000,34 +1005,32 @@ namespace CryptoExchange.Net.Sockets
foreach (var subscription in subList) foreach (var subscription in subList)
{ {
subscription.ConnectionInvocations = 0; subscription.ConnectionInvocations = 0;
if (subscription.Closed) if (!subscription.Active)
// Can be closed during resubscribing // Can be closed during resubscribing
continue; continue;
subscription.IsResubscribing = true; subscription.Status = SubscriptionStatus.Subscribing;
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false); var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
if (!result) if (!result)
{ {
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString()); _logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
subscription.IsResubscribing = false; subscription.Status = SubscriptionStatus.Pending;
return result; return result;
} }
var subQuery = subscription.CreateSubscriptionQuery(this); var subQuery = subscription.CreateSubscriptionQuery(this);
if (subQuery == null) if (subQuery == null)
{ {
subscription.IsResubscribing = false; subscription.Status = SubscriptionStatus.Subscribed;
continue; continue;
} }
var waitEvent = new AsyncResetEvent(false); var waitEvent = new AsyncResetEvent(false);
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) => taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
{ {
subscription.IsResubscribing = false; subscription.Status = r.Result.Success ? SubscriptionStatus.Subscribed: SubscriptionStatus.Pending;
subscription.HandleSubQueryResponse(subQuery.Response!); subscription.HandleSubQueryResponse(subQuery.Response!);
waitEvent.Set(); waitEvent.Set();
if (r.Result.Success)
subscription.Confirmed = true;
return r.Result; return r.Result;
})); }));
} }

View File

@ -34,21 +34,33 @@ namespace CryptoExchange.Net.Sockets
/// Is it a user subscription /// Is it a user subscription
/// </summary> /// </summary>
public bool UserSubscription { get; set; } public bool UserSubscription { get; set; }
private SubscriptionStatus _status;
/// <summary> /// <summary>
/// Has the subscription been confirmed /// Current subscription status
/// </summary> /// </summary>
public bool Confirmed { get; set; } public SubscriptionStatus Status
{
get => _status;
set
{
if (_status == value)
return;
_status = value;
Task.Run(() => StatusChanged?.Invoke(value));
}
}
/// <summary> /// <summary>
/// Is the subscription closed /// Whether the subscription is active
/// </summary> /// </summary>
public bool Closed { get; set; } public bool Active => Status != SubscriptionStatus.Closing && Status != SubscriptionStatus.Closed;
/// <summary> /// <summary>
/// Is the subscription currently resubscribing /// Whether the unsubscribing of this subscription lead to the closing of the connection
/// </summary> /// </summary>
public bool IsResubscribing { get; set; } public bool IsClosingConnection { get; set; }
/// <summary> /// <summary>
/// Logger /// Logger
@ -77,7 +89,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Listener unsubscribed event /// Listener unsubscribed event
/// </summary> /// </summary>
public event Action? Unsubscribed; public event Action<SubscriptionStatus>? StatusChanged;
/// <summary> /// <summary>
/// Subscription topic /// Subscription topic
@ -167,7 +179,7 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public void Reset() public void Reset()
{ {
Confirmed = false; Status = SubscriptionStatus.Pending;
DoHandleReset(); DoHandleReset();
} }
@ -185,24 +197,16 @@ namespace CryptoExchange.Net.Sockets
Exception?.Invoke(e); Exception?.Invoke(e);
} }
/// <summary>
/// Invoke the unsubscribed event
/// </summary>
public void InvokeUnsubscribedHandler()
{
Unsubscribed?.Invoke();
}
/// <summary> /// <summary>
/// State of this subscription /// State of this subscription
/// </summary> /// </summary>
/// <param name="Id">The id of the subscription</param> /// <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="Invocations">Number of times this subscription got a message</param>
/// <param name="ListenMatcher">Matcher for this subscription</param> /// <param name="ListenMatcher">Matcher for this subscription</param>
public record SubscriptionState( public record SubscriptionState(
int Id, int Id,
bool Confirmed, SubscriptionStatus Status,
int Invocations, int Invocations,
MessageMatcher ListenMatcher MessageMatcher ListenMatcher
); );
@ -213,7 +217,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns> /// <returns></returns>
public SubscriptionState GetState() public SubscriptionState GetState()
{ {
return new SubscriptionState(Id, Confirmed, TotalInvocations, MessageMatcher); return new SubscriptionState(Id, Status, TotalInvocations, MessageMatcher);
} }
} }

View File

@ -18,7 +18,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="authenticated"></param> /// <param name="authenticated"></param>
public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false) public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false)
{ {
Confirmed = true; Status = SubscriptionStatus.Subscribed;
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -415,7 +415,7 @@ namespace CryptoExchange.Net.Testing.Comparers
var value = jsonValue.GetDecimal(); var value = jsonValue.GetDecimal();
if (objectValue is DateTime time) 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}"); throw new Exception($"{method}: {property} not equal: {DateTimeConverter.ParseFromDouble((double)value!)} vs {time}");
} }
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true) else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)

View File

@ -325,9 +325,12 @@ namespace CryptoExchange.Net.Trackers.Trades
if (Period != null) if (Period != null)
items = items.Where(e => e.Timestamp >= DateTime.UtcNow.Add(-Period.Value)); items = items.Where(e => e.Timestamp >= DateTime.UtcNow.Add(-Period.Value));
_snapshotId = data.Max(d => d.Timestamp.Ticks); if (items.Any())
foreach (var item in items.OrderBy(d => d.Timestamp)) {
_data.Add(item); _snapshotId = data.Max(d => d.Timestamp.Ticks);
foreach (var item in items.OrderBy(d => d.Timestamp))
_data.Add(item);
}
_snapshotSet = true; _snapshotSet = true;
_changed = true; _changed = true;

View File

@ -5,30 +5,32 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="11.7.1" /> <PackageReference Include="Binance.Net" Version="11.9.0" />
<PackageReference Include="Bitfinex.Net" Version="9.7.0" /> <PackageReference Include="Bitfinex.Net" Version="9.9.0" />
<PackageReference Include="BitMart.Net" Version="2.8.0" /> <PackageReference Include="BitMart.Net" Version="2.10.0" />
<PackageReference Include="BloFin.Net" Version="1.0.0" /> <PackageReference Include="BloFin.Net" Version="1.2.0" />
<PackageReference Include="Bybit.Net" Version="5.8.0" /> <PackageReference Include="Bybit.Net" Version="5.10.1" />
<PackageReference Include="CoinEx.Net" Version="9.7.0" /> <PackageReference Include="CoinEx.Net" Version="9.9.0" />
<PackageReference Include="CoinW.Net" Version="1.4.0" /> <PackageReference Include="CoinW.Net" Version="1.6.0" />
<PackageReference Include="CryptoCom.Net" Version="2.8.0" /> <PackageReference Include="CryptoCom.Net" Version="2.10.0" />
<PackageReference Include="DeepCoin.Net" Version="2.7.0" /> <PackageReference Include="DeepCoin.Net" Version="2.9.0" />
<PackageReference Include="GateIo.Net" Version="2.8.1" /> <PackageReference Include="GateIo.Net" Version="2.11.0" />
<PackageReference Include="HyperLiquid.Net" Version="2.12.0" /> <PackageReference Include="HyperLiquid.Net" Version="2.14.0" />
<PackageReference Include="JK.BingX.Net" Version="2.7.0" /> <PackageReference Include="JK.BingX.Net" Version="2.9.0" />
<PackageReference Include="JK.Bitget.Net" Version="2.7.1" /> <PackageReference Include="JK.Bitget.Net" Version="2.9.0" />
<PackageReference Include="JK.Mexc.Net" Version="3.8.0" /> <PackageReference Include="JK.Mexc.Net" Version="3.10.0" />
<PackageReference Include="JK.OKX.Net" Version="3.7.1" /> <PackageReference Include="JK.OKX.Net" Version="3.9.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="2.7.0" /> <PackageReference Include="Jkorf.Aster.Net" Version="1.1.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="2.7.0" /> <PackageReference Include="JKorf.BitMEX.Net" Version="2.9.0" />
<PackageReference Include="JKorf.HTX.Net" Version="7.7.0" /> <PackageReference Include="JKorf.Coinbase.Net" Version="2.9.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.7.0" /> <PackageReference Include="JKorf.HTX.Net" Version="7.9.0" />
<PackageReference Include="Kucoin.Net" Version="7.7.1" /> <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="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Toobit.Net" Version="1.6.0" /> <PackageReference Include="Toobit.Net" Version="1.8.0" />
<PackageReference Include="WhiteBit.Net" Version="2.8.0" /> <PackageReference Include="WhiteBit.Net" Version="2.10.0" />
<PackageReference Include="XT.Net" Version="2.7.0" /> <PackageReference Include="XT.Net" Version="2.9.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,4 +1,5 @@
@page "/" @page "/"
@inject IAsterRestClient asterClient
@inject IBinanceRestClient binanceClient @inject IBinanceRestClient binanceClient
@inject IBingXRestClient bingXClient @inject IBingXRestClient bingXClient
@inject IBitfinexRestClient bitfinexClient @inject IBitfinexRestClient bitfinexClient
@ -20,6 +21,7 @@
@inject IMexcRestClient mexcClient @inject IMexcRestClient mexcClient
@inject IOKXRestClient okxClient @inject IOKXRestClient okxClient
@inject IToobitRestClient toobitClient @inject IToobitRestClient toobitClient
@inject IUpbitRestClient upbitClient
@inject IWhiteBitRestClient whitebitClient @inject IWhiteBitRestClient whitebitClient
@inject IXTRestClient xtClient @inject IXTRestClient xtClient
@ -34,6 +36,7 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
var asterTask = asterClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT"); var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT"); var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT");
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD"); var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
@ -49,16 +52,20 @@
var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot); var deepCoinTask = deepCoinClient.ExchangeApi.ExchangeData.GetTickersAsync(DeepCoin.Net.Enums.SymbolType.Spot);
var gateioTask = gateioClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT"); var gateioTask = gateioClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT");
var htxTask = htxClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt"); 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 krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT"); var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT"); var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTC-USDT"); var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTC-USDT");
var toobitTask = toobitClient.SpotApi.ExchangeData.GetTickersAsync("BTCUSDT"); var toobitTask = toobitClient.SpotApi.ExchangeData.GetTickersAsync("BTCUSDT");
var upbitTask = upbitClient.SpotApi.ExchangeData.GetTickerAsync("USDT-BTC");
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync(); var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt"); 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) if (binanceTask.Result.Success)
_prices.Add("Binance", binanceTask.Result.Data.LastPrice); _prices.Add("Binance", binanceTask.Result.Data.LastPrice);
@ -131,6 +138,9 @@
if (toobitTask.Result.Success) if (toobitTask.Result.Success)
_prices.Add("Toobit", toobitTask.Result.Data.Single().LastPrice ?? 0); _prices.Add("Toobit", toobitTask.Result.Data.Single().LastPrice ?? 0);
if (upbitTask.Result.Success)
_prices.Add("Upbit", upbitTask.Result.Data.LastPrice);
if (whitebitTask.Result.Success){ if (whitebitTask.Result.Success){
// WhiteBit API doesn't offer an endpoint to filter for a specific ticker, so we have to filter client side // 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; var tickers = whitebitTask.Result.Data;

View File

@ -1,4 +1,5 @@
@page "/LiveData" @page "/LiveData"
@inject IAsterSocketClient asterSocketClient
@inject IBinanceSocketClient binanceSocketClient @inject IBinanceSocketClient binanceSocketClient
@inject IBingXSocketClient bingXSocketClient @inject IBingXSocketClient bingXSocketClient
@inject IBitfinexSocketClient bitfinexSocketClient @inject IBitfinexSocketClient bitfinexSocketClient
@ -20,6 +21,7 @@
@inject IMexcSocketClient mexcSocketClient @inject IMexcSocketClient mexcSocketClient
@inject IOKXSocketClient okxSocketClient @inject IOKXSocketClient okxSocketClient
@inject IToobitSocketClient toobitSocketClient @inject IToobitSocketClient toobitSocketClient
@inject IUpbitSocketClient upbitSocketClient
@inject IWhiteBitSocketClient whitebitSocketClient @inject IWhiteBitSocketClient whitebitSocketClient
@inject IXTSocketClient xtSocketClient @inject IXTSocketClient xtSocketClient
@using System.Collections.Concurrent @using System.Collections.Concurrent
@ -43,6 +45,8 @@
{ {
var tasks = new Task<CallResult<UpdateSubscription>>[] 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)), binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)), bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)), bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
@ -69,6 +73,7 @@
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)), okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
// Toobit doesn't support the ETH/BTC pair // Toobit doesn't support the ETH/BTC pair
//toobitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Toobit", data.Data.LastPrice ?? 0)), //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)), whitebitSocketClient.V4Api.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("WhiteBit", data.Data.Ticker.LastPrice)),
}; };

View File

@ -1,6 +1,7 @@
@page "/OrderBooks" @page "/OrderBooks"
@using System.Collections.Concurrent @using System.Collections.Concurrent
@using System.Timers @using System.Timers
@using Aster.Net.Interfaces
@using Binance.Net.Interfaces @using Binance.Net.Interfaces
@using BingX.Net.Interfaces @using BingX.Net.Interfaces
@using Bitfinex.Net.Interfaces @using Bitfinex.Net.Interfaces
@ -24,9 +25,11 @@
@using Kucoin.Net.Interfaces @using Kucoin.Net.Interfaces
@using Mexc.Net.Interfaces @using Mexc.Net.Interfaces
@using OKX.Net.Interfaces; @using OKX.Net.Interfaces;
@using Upbit.Net.Interfaces;
@using Toobit.Net.Interfaces; @using Toobit.Net.Interfaces;
@using WhiteBit.Net.Interfaces @using WhiteBit.Net.Interfaces
@using XT.Net.Interfaces @using XT.Net.Interfaces
@inject IAsterOrderBookFactory asterFactory
@inject IBinanceOrderBookFactory binanceFactory @inject IBinanceOrderBookFactory binanceFactory
@inject IBingXOrderBookFactory bingXFactory @inject IBingXOrderBookFactory bingXFactory
@inject IBitfinexOrderBookFactory bitfinexFactory @inject IBitfinexOrderBookFactory bitfinexFactory
@ -48,6 +51,7 @@
@inject IMexcOrderBookFactory mexcFactory @inject IMexcOrderBookFactory mexcFactory
@inject IOKXOrderBookFactory okxFactory @inject IOKXOrderBookFactory okxFactory
@inject IToobitOrderBookFactory toobitFactory @inject IToobitOrderBookFactory toobitFactory
@inject IUpbitOrderBookFactory upbitFactory
@inject IWhiteBitOrderBookFactory whitebitFactory @inject IWhiteBitOrderBookFactory whitebitFactory
@inject IXTOrderBookFactory xtFactory @inject IXTOrderBookFactory xtFactory
@implements IDisposable @implements IDisposable
@ -83,6 +87,7 @@
_books = new Dictionary<string, ISymbolOrderBook> _books = new Dictionary<string, ISymbolOrderBook>
{ {
{ "Aster", binanceFactory.CreateSpot("ETHUSDT") },
{ "Binance", binanceFactory.CreateSpot("ETHBTC") }, { "Binance", binanceFactory.CreateSpot("ETHBTC") },
{ "BingX", bingXFactory.CreateSpot("ETH-BTC") }, { "BingX", bingXFactory.CreateSpot("ETH-BTC") },
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") }, { "Bitfinex", bitfinexFactory.Create("tETHBTC") },
@ -104,6 +109,7 @@
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") }, { "Mexc", mexcFactory.CreateSpot("ETHBTC") },
{ "OKX", okxFactory.Create("ETH-BTC") }, { "OKX", okxFactory.Create("ETH-BTC") },
{ "Toobit", toobitFactory.CreateSpot("ETHUSDT") }, { "Toobit", toobitFactory.CreateSpot("ETHUSDT") },
{ "Upbit", upbitFactory.CreateSpot("BTC-ETH") },
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") }, { "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
{ "XT", xtFactory.CreateSpot("eth_btc") }, { "XT", xtFactory.CreateSpot("eth_btc") },
}; };

View File

@ -1,6 +1,7 @@
@page "/Trackers" @page "/Trackers"
@using System.Collections.Concurrent @using System.Collections.Concurrent
@using System.Timers @using System.Timers
@using Aster.Net.Interfaces
@using Binance.Net.Interfaces @using Binance.Net.Interfaces
@using BingX.Net.Interfaces @using BingX.Net.Interfaces
@using Bitfinex.Net.Interfaces @using Bitfinex.Net.Interfaces
@ -25,9 +26,11 @@
@using Kucoin.Net.Interfaces @using Kucoin.Net.Interfaces
@using Mexc.Net.Interfaces @using Mexc.Net.Interfaces
@using OKX.Net.Interfaces; @using OKX.Net.Interfaces;
@using Upbit.Net.Interfaces;
@using Toobit.Net.Interfaces; @using Toobit.Net.Interfaces;
@using WhiteBit.Net.Interfaces @using WhiteBit.Net.Interfaces
@using XT.Net.Interfaces @using XT.Net.Interfaces
@inject IAsterTrackerFactory asterFactory
@inject IBinanceTrackerFactory binanceFactory @inject IBinanceTrackerFactory binanceFactory
@inject IBingXTrackerFactory bingXFactory @inject IBingXTrackerFactory bingXFactory
@inject IBitfinexTrackerFactory bitfinexFactory @inject IBitfinexTrackerFactory bitfinexFactory
@ -49,11 +52,12 @@
@inject IMexcTrackerFactory mexcFactory @inject IMexcTrackerFactory mexcFactory
@inject IOKXTrackerFactory okxFactory @inject IOKXTrackerFactory okxFactory
@inject IToobitTrackerFactory toobitFactory @inject IToobitTrackerFactory toobitFactory
@inject IUpbitTrackerFactory upbitFactory
@inject IWhiteBitTrackerFactory whitebitFactory @inject IWhiteBitTrackerFactory whitebitFactory
@inject IXTTrackerFactory xtFactory @inject IXTTrackerFactory xtFactory
@implements IDisposable @implements IDisposable
<h3>ETH-BTC trade Trackers, live updates:</h3> <h3>Trade Trackers, live updates:</h3>
<div style="display:flex; flex-wrap: wrap;"> <div style="display:flex; flex-wrap: wrap;">
@foreach (var tracker in _trackers.OrderBy(p => p.Exchange)) @foreach (var tracker in _trackers.OrderBy(p => p.Exchange))
{ {
@ -79,6 +83,7 @@
_trackers = new List<ITradeTracker> _trackers = new List<ITradeTracker>
{ {
{ asterFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ binanceFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) }, { binanceFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bingXFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) }, { bingXFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ bitfinexFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) }, { bitfinexFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
@ -100,11 +105,12 @@
{ mexcFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) }, { mexcFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ okxFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) }, { okxFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ toobitFactory.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)) }, { whitebitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ xtFactory.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 // Use a manual update timer so the page isn't refreshed too often
_timer = new Timer(500); _timer = new Timer(500);

View File

@ -33,6 +33,7 @@ namespace BlazorClient
restOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET"); restOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
}); });
services.AddAster();
services.AddBingX(); services.AddBingX();
services.AddBitfinex(); services.AddBitfinex();
services.AddBitget(); services.AddBitget();
@ -53,6 +54,7 @@ namespace BlazorClient
services.AddMexc(); services.AddMexc();
services.AddOKX(); services.AddOKX();
services.AddToobit(); services.AddToobit();
services.AddUpbit();
services.AddWhiteBit(); services.AddWhiteBit();
services.AddXT(); services.AddXT();
} }

View File

@ -8,6 +8,7 @@
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using BlazorClient @using BlazorClient
@using BlazorClient.Shared @using BlazorClient.Shared
@using Aster.Net.Interfaces.Clients;
@using Binance.Net.Interfaces.Clients; @using Binance.Net.Interfaces.Clients;
@using BingX.Net.Interfaces.Clients; @using BingX.Net.Interfaces.Clients;
@using Bitfinex.Net.Interfaces.Clients; @using Bitfinex.Net.Interfaces.Clients;
@ -28,6 +29,7 @@
@using Kucoin.Net.Interfaces.Clients; @using Kucoin.Net.Interfaces.Clients;
@using Mexc.Net.Interfaces.Clients; @using Mexc.Net.Interfaces.Clients;
@using OKX.Net.Interfaces.Clients; @using OKX.Net.Interfaces.Clients;
@using Upbit.Net.Interfaces.Clients;
@using Toobit.Net.Interfaces.Clients; @using Toobit.Net.Interfaces.Clients;
@using WhiteBit.Net.Interfaces.Clients @using WhiteBit.Net.Interfaces.Clients
@using XT.Net.Interfaces.Clients @using XT.Net.Interfaces.Clients

View File

@ -6,20 +6,20 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="11.1.0" /> <PackageReference Include="Binance.Net" Version="11.9.0" />
<PackageReference Include="Bitfinex.Net" Version="9.1.0" /> <PackageReference Include="Bitfinex.Net" Version="9.9.0" />
<PackageReference Include="BitMart.Net" Version="2.1.0" /> <PackageReference Include="BitMart.Net" Version="2.10.0" />
<PackageReference Include="Bybit.Net" Version="5.1.0" /> <PackageReference Include="Bybit.Net" Version="5.10.1" />
<PackageReference Include="CoinEx.Net" Version="9.1.0" /> <PackageReference Include="CoinEx.Net" Version="9.9.0" />
<PackageReference Include="CryptoCom.Net" Version="2.1.0" /> <PackageReference Include="CryptoCom.Net" Version="2.10.0" />
<PackageReference Include="GateIo.Net" Version="2.1.0" /> <PackageReference Include="GateIo.Net" Version="2.11.0" />
<PackageReference Include="JK.Bitget.Net" Version="2.1.0" /> <PackageReference Include="JK.Bitget.Net" Version="2.9.0" />
<PackageReference Include="JK.Mexc.Net" Version="3.1.0" /> <PackageReference Include="JK.Mexc.Net" Version="3.10.0" />
<PackageReference Include="JK.OKX.Net" Version="3.1.0" /> <PackageReference Include="JK.OKX.Net" Version="3.9.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="2.1.0" /> <PackageReference Include="JKorf.Coinbase.Net" Version="2.9.0" />
<PackageReference Include="JKorf.HTX.Net" Version="7.1.0" /> <PackageReference Include="JKorf.HTX.Net" Version="7.9.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.1.0" /> <PackageReference Include="KrakenExchange.Net" Version="6.9.0" />
<PackageReference Include="Kucoin.Net" Version="7.1.0" /> <PackageReference Include="Kucoin.Net" Version="7.9.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -8,9 +8,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="11.1.0" /> <PackageReference Include="Binance.Net" Version="11.9.0" />
<PackageReference Include="BitMart.Net" Version="2.1.0" /> <PackageReference Include="BitMart.Net" Version="2.10.0" />
<PackageReference Include="JK.OKX.Net" Version="3.1.0" /> <PackageReference Include="JK.OKX.Net" Version="3.9.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -12,6 +12,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
||Exchange|Type|Repository|Nuget|Referral Link|Referral Fee Discount| ||Exchange|Type|Repository|Nuget|Referral Link|Referral Fee Discount|
|--|--|--|--|--|--|--| |--|--|--|--|--|--|--|
|![Aster](https://raw.githubusercontent.com/JKorf/Aster.Net/refs/heads/main/Aster.Net/Icon/icon.png)|Aster|DEX|[JKorf/Aster.Net](https://github.com/JKorf/Aster.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Aster.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Aster.Net)|[Link](https://www.asterdex.com/en/referral/FD2E11)|4%|
|![Binance](https://raw.githubusercontent.com/JKorf/Binance.Net/refs/heads/master/Binance.Net/Icon/icon.png)|Binance|CEX|[JKorf/Binance.Net](https://github.com/JKorf/Binance.Net)|[![Nuget version](https://img.shields.io/nuget/v/Binance.net.svg?style=flat-square)](https://www.nuget.org/packages/Binance.Net)|[Link](https://accounts.binance.com/register?ref=X5K3F2ZG)|20%| |![Binance](https://raw.githubusercontent.com/JKorf/Binance.Net/refs/heads/master/Binance.Net/Icon/icon.png)|Binance|CEX|[JKorf/Binance.Net](https://github.com/JKorf/Binance.Net)|[![Nuget version](https://img.shields.io/nuget/v/Binance.net.svg?style=flat-square)](https://www.nuget.org/packages/Binance.Net)|[Link](https://accounts.binance.com/register?ref=X5K3F2ZG)|20%|
|![BingX](https://raw.githubusercontent.com/JKorf/BingX.Net/refs/heads/main/BingX.Net/Icon/BingX.png)|BingX|CEX|[JKorf/BingX.Net](https://github.com/JKorf/BingX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.BingX.Net)|[Link](https://bingx.com/invite/FFHRJKWG/)|20%| |![BingX](https://raw.githubusercontent.com/JKorf/BingX.Net/refs/heads/main/BingX.Net/Icon/BingX.png)|BingX|CEX|[JKorf/BingX.Net](https://github.com/JKorf/BingX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.BingX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.BingX.Net)|[Link](https://bingx.com/invite/FFHRJKWG/)|20%|
|![Bitfinex](https://raw.githubusercontent.com/JKorf/Bitfinex.Net/refs/heads/master/Bitfinex.Net/Icon/icon.png)|Bitfinex|CEX|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitfinex.Net)|-|-| |![Bitfinex](https://raw.githubusercontent.com/JKorf/Bitfinex.Net/refs/heads/master/Bitfinex.Net/Icon/icon.png)|Bitfinex|CEX|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitfinex.Net)|-|-|
@ -34,6 +35,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|![Mexc](https://raw.githubusercontent.com/JKorf/Mexc.Net/refs/heads/main/Mexc.Net/Icon/icon.png)|Mexc|CEX|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Mexc.Net)|-|-| |![Mexc](https://raw.githubusercontent.com/JKorf/Mexc.Net/refs/heads/main/Mexc.Net/Icon/icon.png)|Mexc|CEX|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Mexc.Net)|-|-|
|![OKX](https://raw.githubusercontent.com/JKorf/OKX.Net/refs/heads/main/OKX.Net/Icon/icon.png)|OKX|CEX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)|[Link](https://www.okx.com/join/14592495)|20%| |![OKX](https://raw.githubusercontent.com/JKorf/OKX.Net/refs/heads/main/OKX.Net/Icon/icon.png)|OKX|CEX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)|[Link](https://www.okx.com/join/14592495)|20%|
|![Toobit](https://raw.githubusercontent.com/JKorf/Toobit.Net/refs/heads/main/Toobit.Net/Icon/icon.png)|Toobit|CEX|[JKorf/Toobit.Net](https://github.com/JKorf/Toobit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Toobit.net.svg?style=flat-square)](https://www.nuget.org/packages/Toobit.Net)|[Link](https://www.toobit.com/en-US/register?invite_code=zsV19h)|-| |![Toobit](https://raw.githubusercontent.com/JKorf/Toobit.Net/refs/heads/main/Toobit.Net/Icon/icon.png)|Toobit|CEX|[JKorf/Toobit.Net](https://github.com/JKorf/Toobit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Toobit.net.svg?style=flat-square)](https://www.nuget.org/packages/Toobit.Net)|[Link](https://www.toobit.com/en-US/register?invite_code=zsV19h)|-|
|![Upbit](https://raw.githubusercontent.com/JKorf/Upbit.Net/refs/heads/main/Upbit.Net/Icon/icon.png)|Upbit|CEX|[JKorf/Upbit.Net](https://github.com/JKorf/Upbit.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Upbit.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Upbit.Net)|-|-|
|![WhiteBit](https://raw.githubusercontent.com/JKorf/WhiteBit.Net/refs/heads/main/WhiteBit.Net/Icon/icon.png)|WhiteBit|CEX|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[![Nuget version](https://img.shields.io/nuget/v/WhiteBit.net.svg?style=flat-square)](https://www.nuget.org/packages/WhiteBit.Net)|[Link](https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf)|-| |![WhiteBit](https://raw.githubusercontent.com/JKorf/WhiteBit.Net/refs/heads/main/WhiteBit.Net/Icon/icon.png)|WhiteBit|CEX|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[![Nuget version](https://img.shields.io/nuget/v/WhiteBit.net.svg?style=flat-square)](https://www.nuget.org/packages/WhiteBit.Net)|[Link](https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf)|-|
|![XT](https://raw.githubusercontent.com/JKorf/XT.Net/refs/heads/main/XT.Net/Icon/icon.png)|XT|CEX|[JKorf/XT.Net](https://github.com/JKorf/XT.Net)|[![Nuget version](https://img.shields.io/nuget/v/XT.net.svg?style=flat-square)](https://www.nuget.org/packages/XT.Net)|[Link](https://www.xt.com/ru/accounts/register?ref=CZG39C)|25%| |![XT](https://raw.githubusercontent.com/JKorf/XT.Net/refs/heads/main/XT.Net/Icon/icon.png)|XT|CEX|[JKorf/XT.Net](https://github.com/JKorf/XT.Net)|[![Nuget version](https://img.shields.io/nuget/v/XT.net.svg?style=flat-square)](https://www.nuget.org/packages/XT.Net)|[Link](https://www.xt.com/ru/accounts/register?ref=CZG39C)|25%|
@ -64,6 +66,18 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf). Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes ## Release notes
* Version 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 * Version 9.8.0 - 30 Sep 2025
* Added ContractAddress to SharedAsset model * Added ContractAddress to SharedAsset model
* Added ITrackerFactory interface * Added ITrackerFactory interface