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

Compare commits

..

14 Commits

21 changed files with 337 additions and 90 deletions
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
<Authors>JKorf</Authors>
<Description>Protobuf support for CryptoExchange.Net</Description>
<PackageVersion>9.6.0</PackageVersion>
<AssemblyVersion>9.6.0</AssemblyVersion>
<FileVersion>9.6.0</FileVersion>
<PackageVersion>9.8.0</PackageVersion>
<AssemblyVersion>9.8.0</AssemblyVersion>
<FileVersion>9.8.0</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.6.0" />
<PackageReference Include="CryptoExchange.Net" Version="9.8.0" />
<PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup>
</Project>
+6
View File
@@ -5,6 +5,12 @@
Protobuf support for CryptoExchange.Net.
## Release notes
* 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/
* Version 9.6.0 - 25 Aug 2025
* Updated CryptoExchange.Net version to 9.6.0
@@ -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);
+47 -13
View File
@@ -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);
@@ -343,7 +343,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 +494,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 +551,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 +750,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();
@@ -140,10 +140,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))
{
@@ -204,6 +204,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
+3 -3
View File
@@ -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.8.0</PackageVersion>
<AssemblyVersion>9.8.0</AssemblyVersion>
<FileVersion>9.8.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>
-2
View File
@@ -88,8 +88,6 @@ namespace CryptoExchange.Net
value -= offset;
else value += (step.Value - offset);
}
value = RoundDown(value, 8);
return value.Normalize();
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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,21 @@ namespace CryptoExchange.Net.Objects.Sockets
private readonly SocketConnection _connection;
private readonly Subscription _listener;
private object _eventLock = new object();
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 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 +36,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 +45,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 +56,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 +65,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 +74,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 +105,85 @@ 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.Unsubscribed += HandleUnsubscribed;
}
private void HandleUnsubscribed()
{
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
_connection.ConnectionLost -= HandleConnectionLostEvent;
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
_connection.ActivityPaused -= HandlePausedEvent;
_connection.ActivityUnpaused -= HandleUnpausedEvent;
}
private void HandleConnectionClosedEvent()
{
List<Action> handlers;
lock (_eventLock)
handlers = _connectionClosedEventHandlers.ToList();
foreach(var callback in handlers)
callback();
}
private void HandleConnectionLostEvent()
{
List<Action> handlers;
lock (_eventLock)
handlers = _connectionLostEventHandlers.ToList();
foreach (var callback in handlers)
callback();
}
private void HandleConnectionRestoredEvent(TimeSpan period)
{
List<Action<TimeSpan>> handlers;
lock (_eventLock)
handlers = _connectionRestoredEventHandlers.ToList();
foreach (var callback in handlers)
callback(period);
}
private void HandleResubscribeFailedEvent(Error error)
{
List<Action<Error>> handlers;
lock (_eventLock)
handlers = _resubscribeFailedEventHandlers.ToList();
foreach (var callback in handlers)
callback(error);
}
private void HandlePausedEvent()
{
List<Action> handlers;
lock (_eventLock)
handlers = _activityPausedEventHandlers.ToList();
foreach (var callback in handlers)
callback();
}
private void HandleUnpausedEvent()
{
List<Action> handlers;
lock (_eventLock)
handlers = _activityUnpausedEventHandlers.ToList();
foreach (var callback in handlers)
callback();
}
/// <summary>
@@ -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
@@ -706,6 +706,8 @@ namespace CryptoExchange.Net.Sockets
lock (_listenersLock)
_listeners.Remove(subscription);
subscription.InvokeUnsubscribedHandler();
}
/// <summary>
@@ -74,6 +74,10 @@ namespace CryptoExchange.Net.Sockets
/// Exception event
/// </summary>
public event Action<Exception>? Exception;
/// <summary>
/// Listener unsubscribed event
/// </summary>
public event Action? Unsubscribed;
/// <summary>
/// Subscription topic
@@ -181,6 +185,14 @@ namespace CryptoExchange.Net.Sockets
Exception?.Invoke(e);
}
/// <summary>
/// Invoke the unsubscribed event
/// </summary>
public void InvokeUnsubscribedHandler()
{
Unsubscribed?.Invoke();
}
/// <summary>
/// State of this subscription
/// </summary>
+23 -22
View File
@@ -5,29 +5,30 @@
</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.7.1" />
<PackageReference Include="Bitfinex.Net" Version="9.7.0" />
<PackageReference Include="BitMart.Net" Version="2.8.0" />
<PackageReference Include="BloFin.Net" Version="1.0.0" />
<PackageReference Include="Bybit.Net" Version="5.8.0" />
<PackageReference Include="CoinEx.Net" Version="9.7.0" />
<PackageReference Include="CoinW.Net" Version="1.4.0" />
<PackageReference Include="CryptoCom.Net" Version="2.8.0" />
<PackageReference Include="DeepCoin.Net" Version="2.7.0" />
<PackageReference Include="GateIo.Net" Version="2.8.1" />
<PackageReference Include="HyperLiquid.Net" Version="2.12.0" />
<PackageReference Include="JK.BingX.Net" Version="2.7.0" />
<PackageReference Include="JK.Bitget.Net" Version="2.7.1" />
<PackageReference Include="JK.Mexc.Net" Version="3.8.0" />
<PackageReference Include="JK.OKX.Net" Version="3.7.1" />
<PackageReference Include="JKorf.BitMEX.Net" Version="2.7.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="2.7.0" />
<PackageReference Include="JKorf.HTX.Net" Version="7.7.0" />
<PackageReference Include="KrakenExchange.Net" Version="6.7.0" />
<PackageReference Include="Kucoin.Net" Version="7.7.1" />
<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.6.0" />
<PackageReference Include="WhiteBit.Net" Version="2.8.0" />
<PackageReference Include="XT.Net" Version="2.7.0" />
</ItemGroup>
</Project>
+5
View File
@@ -5,6 +5,7 @@
@inject IBitgetRestClient bitgetClient
@inject IBitMartRestClient bitmartClient
@inject IBitMEXRestClient bitmexClient
@inject IBloFinRestClient bloFinClient
@inject IBybitRestClient bybitClient
@inject ICoinbaseRestClient coinbaseClient
@inject ICoinExRestClient coinexClient
@@ -39,6 +40,7 @@
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"]);
@@ -76,6 +78,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);
+5 -1
View File
@@ -5,6 +5,7 @@
@inject IBitgetSocketClient bitgetSocketClient
@inject IBitMartSocketClient bitmartSocketClient
@inject IBitMEXSocketClient bitmexSocketClient
@inject IBloFinSocketClient bloFinSocketClient
@inject IBybitSocketClient bybitSocketClient
@inject ICoinbaseSocketClient coinbaseSocketClient
@inject ICoinExSocketClient coinExSocketClient
@@ -48,6 +49,8 @@
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,7 +64,8 @@
//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)),
+7 -7
View File
@@ -7,6 +7,7 @@
@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
@@ -32,6 +33,7 @@
@inject IBitgetOrderBookFactory bitgetFactory
@inject IBitMartOrderBookFactory bitmartFactory
@inject IBitMEXOrderBookFactory bitmexFactory
@inject IBloFinOrderBookFactory bloFinFactory
@inject IBybitOrderBookFactory bybitFactory
@inject ICoinbaseOrderBookFactory coinbaseFactory
@inject ICoinExOrderBookFactory coinExFactory
@@ -55,7 +57,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++)
@@ -87,23 +89,21 @@
{ "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") },
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
{ "XT", xtFactory.CreateSpot("eth_btc") },
};
+28 -24
View File
@@ -7,6 +7,7 @@
@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
@@ -33,6 +34,7 @@
@inject IBitgetTrackerFactory bitgetFactory
@inject IBitMartTrackerFactory bitmartFactory
@inject IBitMEXTrackerFactory bitmexFactory
@inject IBloFinTrackerFactory bloFinFactory
@inject IBybitTrackerFactory bybitFactory
@inject ICoinbaseTrackerFactory coinbaseFactory
@inject ICoinExTrackerFactory coinExFactory
@@ -71,33 +73,35 @@
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)) },
{ 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)) },
{ whitebitFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
{ xtFactory.CreateTradeTracker(usdtSpotSymbol, period: TimeSpan.FromMinutes(5)) },
};
await Task.WhenAll(_trackers.Select(b => b.StartAsync()));
+1
View File
@@ -38,6 +38,7 @@ namespace BlazorClient
services.AddBitget();
services.AddBitMart();
services.AddBitMEX();
services.AddBloFin();
services.AddBybit();
services.AddCoinbase();
services.AddCoinEx();
+1
View File
@@ -14,6 +14,7 @@
@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;
+13
View File
@@ -18,6 +18,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|![Bitget](https://raw.githubusercontent.com/JKorf/Bitget.Net/refs/heads/main/Bitget.Net/Icon/icon.png)|Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%|
|![BitMart](https://raw.githubusercontent.com/JKorf/BitMart.Net/refs/heads/main/BitMart.Net/Icon/icon.png)|BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[![Nuget version](https://img.shields.io/nuget/v/BitMart.net.svg?style=flat-square)](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%|
|![BitMEX](https://raw.githubusercontent.com/JKorf/BitMEX.Net/refs/heads/main/BitMEX.Net/Icon/icon.png)|BitMEX|CEX|[JKorf/BitMEX.Net](https://github.com/JKorf/BitMEX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.BitMEX.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.BitMEX.Net)|[Link](https://www.bitmex.com/app/register/94f98e)|30%|
|![BloFin](https://raw.githubusercontent.com/JKorf/BloFin.Net/refs/heads/main/BloFin.Net/Icon/icon.png)|BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[![Nuget version](https://img.shields.io/nuget/v/BloFin.net.svg?style=flat-square)](https://www.nuget.org/packages/BloFin.Net)|-|-|
|![Bybit](https://raw.githubusercontent.com/JKorf/Bybit.Net/refs/heads/main/ByBit.Net/Icon/icon.png)|Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-|
|![Coinbase](https://raw.githubusercontent.com/JKorf/Coinbase.Net/refs/heads/main/Coinbase.Net/Icon/icon.png)|Coinbase|CEX|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Coinbase.Net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Coinbase.Net)|[Link](https://advanced.coinbase.com/join/T6H54H8)|-|
|![CoinEx](https://raw.githubusercontent.com/JKorf/CoinEx.Net/refs/heads/master/CoinEx.Net/Icon/icon.png)|CoinEx|CEX|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)|[Link](https://www.coinex.com/register?rc=rbtnp)|20%|
@@ -38,6 +39,10 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
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
[![Nuget version](https://img.shields.io/discord/847020490588422145?style=for-the-badge)](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 +64,14 @@ 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.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