1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-11 16:32:57 +00:00

Compare commits

...

14 Commits

31 changed files with 712 additions and 1288 deletions
+4
View File
@@ -38,6 +38,10 @@ After calling `GetSpotSymbolsAsync`, `ISpotSymbolRestClient.SpotSymbolCatalog` m
When implementing an exchange library, use `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` only as best-effort classifiers and supply exchange-specific additions where needed.
## Shared market-data quantities
In 12.4.0, use `SharedOrderQuantity`-valued `Volumes` on shared spot/futures tickers and klines, and `Quantities` on shared trades. The scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
## Result pattern
REST methods return `HttpResult<T>` and websocket subscription methods return `WebSocketResult<UpdateSubscription>`. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging.
+4
View File
@@ -30,6 +30,10 @@ CryptoExchange.Net 12.2.0 classifies the base and quote sides of `SharedSpotSymb
`ISpotSymbolRestClient.SpotSymbolCatalog` is populated by `GetSpotSymbolsAsync`; `IFuturesSymbolRestClient.FuturesSymbolCatalog` is populated by `GetFuturesSymbolsAsync`. Do not assume a catalog is available before that request. For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` offer best-effort classification and can be extended with exchange-specific values.
## Shared market-data quantities
CryptoExchange.Net 12.4.0 uses `SharedOrderQuantity` for market-data quantities. Prefer `Volumes` on `SharedSpotTicker`, `SharedFuturesTicker`, and `SharedKline`, and `Quantities` on `SharedTrade`; the scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
## Single-exchange code uses the exchange's own client
For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `AGENTS.md`). SharedApis is for portability — use it when you need that.
+4
View File
@@ -85,6 +85,10 @@ After calling `GetSpotSymbolsAsync` or `GetFuturesSymbolsAsync`, use the client'
For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` provide best-effort classification of known assets and accept exchange-specific additions. These helpers are heuristics, not an exhaustive source of truth.
## Shared Market-Data Quantities
Since CryptoExchange.Net 12.4.0, shared market-data models use `SharedOrderQuantity` so base-asset, quote-asset, and contract quantities remain explicit. Read `SharedSpotTicker.Volumes`, `SharedFuturesTicker.Volumes`, and `SharedKline.Volumes`; read `SharedTrade.Quantities`. The former scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
## Available Shared Interfaces
**REST:**
@@ -96,23 +96,20 @@ namespace CryptoExchange.Net.UnitTests
waiters.Add(evnt.WaitAsync());
}
List<bool>? results = null;
var resultsWaiter = Task.Run(async () =>
{
await Task.WhenAll(waiters);
results = waiters.Select(w => w.Result).ToList();
});
var remaining = waiters.ToList();
for(var i = 1; i <= 10; i++)
for (var i = 0; i < 10; i++)
{
evnt.Set();
await Task.Delay(1); // Wait for the continuation.
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
var completed = await Task.WhenAny(remaining);
Assert.That(await completed, Is.True);
remaining.Remove(completed);
Assert.That(remaining.Count(w => w.IsCompleted), Is.Zero);
}
await resultsWaiter;
Assert.That(10 == results?.Count(r => r));
Assert.That(remaining, Is.Empty);
}
[Test]
@@ -0,0 +1,136 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.Default;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class ManualUpdateSubscriptionTests
{
[Test]
public void Constructor_Should_CreateSubscribedVirtualSubscription()
{
var controller = new ManualUpdateSubscription(socketId: 12);
Assert.That(controller.Subscription.SocketId, Is.EqualTo(12));
Assert.That(controller.Subscription.Id, Is.GreaterThan(0));
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Connected));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Subscribed));
Assert.That(controller.Subscription.LastReceiveTime, Is.Null);
}
[Test]
public void StateChanges_Should_BeVisibleOnSubscription()
{
var controller = new ManualUpdateSubscription();
var timestamp = new DateTime(2026, 8, 5, 12, 0, 0, DateTimeKind.Utc);
var statuses = new List<SubscriptionStatus>();
controller.Subscription.SubscriptionStatusChanged += statuses.Add;
controller.SetLastReceiveTime(timestamp);
controller.SetSocketStatus(SocketStatus.Reconnecting);
controller.SetSubscriptionStatus(SubscriptionStatus.Subscribing);
controller.SetSubscriptionStatus(SubscriptionStatus.Subscribed);
Assert.That(controller.Subscription.LastReceiveTime, Is.EqualTo(timestamp));
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Reconnecting));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Subscribed));
Assert.That(statuses, Is.EqualTo(new[]
{
SubscriptionStatus.Subscribing,
SubscriptionStatus.Subscribed
}));
}
[Test]
public void LifecycleMethods_Should_InvokeSubscriptionEvents()
{
var controller = new ManualUpdateSubscription();
var error = new ServerError("Test error", ErrorInfo.Unknown);
var exception = new InvalidOperationException("Test exception");
var disconnectedPeriod = TimeSpan.FromMinutes(2);
var lost = 0;
var restored = TimeSpan.Zero;
Error? resubscribeError = null;
var paused = 0;
var unpaused = 0;
Exception? receivedException = null;
controller.Subscription.ConnectionLost += () => lost++;
controller.Subscription.ConnectionRestored += x => restored = x;
controller.Subscription.ResubscribingFailed += x => resubscribeError = x;
controller.Subscription.ActivityPaused += () => paused++;
controller.Subscription.ActivityUnpaused += () => unpaused++;
controller.Subscription.Exception += x => receivedException = x;
controller.InvokeConnectionLost();
controller.InvokeConnectionRestored(disconnectedPeriod);
controller.InvokeResubscribingFailed(error);
controller.InvokeActivityPaused();
controller.InvokeActivityUnpaused();
controller.InvokeException(exception);
Assert.That(lost, Is.EqualTo(1));
Assert.That(restored, Is.EqualTo(disconnectedPeriod));
Assert.That(resubscribeError, Is.SameAs(error));
Assert.That(paused, Is.EqualTo(1));
Assert.That(unpaused, Is.EqualTo(1));
Assert.That(receivedException, Is.SameAs(exception));
}
[Test]
public void InvokeConnectionClosed_Should_CloseAndOnlyInvokeOnce()
{
var controller = new ManualUpdateSubscription();
var closed = 0;
controller.Subscription.ConnectionClosed += () => closed++;
controller.InvokeConnectionClosed();
controller.InvokeConnectionClosed();
Assert.That(closed, Is.EqualTo(1));
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Closed));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Closed));
}
[Test]
public async Task SubscriptionOperations_Should_InvokeCallbacks()
{
var closes = 0;
var reconnects = 0;
var resubscribes = 0;
var controller = new ManualUpdateSubscription(
closeAsync: () =>
{
closes++;
return Task.CompletedTask;
},
reconnectAsync: () =>
{
reconnects++;
return Task.CompletedTask;
},
resubscribeAsync: () =>
{
resubscribes++;
return Task.FromResult(CallResult.Ok());
});
await controller.Subscription.ReconnectAsync();
var resubscribeResult = await controller.Subscription.ResubscribeAsync();
await controller.Subscription.CloseAsync();
await controller.Subscription.CloseAsync();
Assert.That(reconnects, Is.EqualTo(1));
Assert.That(resubscribes, Is.EqualTo(1));
Assert.That(resubscribeResult.Success, Is.True);
Assert.That(closes, Is.EqualTo(1));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Closed));
}
}
}
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
stream.Seek(0, SeekOrigin.Begin);
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
var data = new string(dataSnippet, 0, written);
errorMsg += $": {data}";
errorMsg += $": {(string.IsNullOrEmpty(data) ? "(empty)" : data)}";
if (data.Length == _errorResponseSnippetLimit)
errorMsg += " (truncated)";
}
+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>12.3.0</PackageVersion>
<AssemblyVersion>12.3.0</AssemblyVersion>
<FileVersion>12.3.0</FileVersion>
<PackageVersion>12.4.0</PackageVersion>
<AssemblyVersion>12.4.0</AssemblyVersion>
<FileVersion>12.4.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType>
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string, string>(
LogLevel.Warning,
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
"[Sckt {SocketId}] received message not matched to any listener. TypeIdentifier: {TypeIdentifier}, ListenId: {ListenId}, current listeners: [{ListenIds}]");
"[Sckt {SocketId}] received message not matched to any listener. TypeIdentifier: {TypeIdentifier}, TopicFilter: {ListenId}, registered TopicFilters for type: [{TopicFilters}]");
_failedToParse = LoggerMessage.Define<int, string>(
LogLevel.Warning,
@@ -326,9 +326,9 @@ namespace CryptoExchange.Net.Logging.Extensions
_sendingData(logger, socketId, requestId, data, null);
}
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string typeIdentifier, string listenId, string listenIds)
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string typeIdentifier, string topicFilter, string topicFilters)
{
_receivedMessageNotMatchedToAnyListener(logger, socketId, typeIdentifier, listenId, listenIds, null);
_receivedMessageNotMatchedToAnyListener(logger, socketId, typeIdentifier, topicFilter, topicFilters, null);
}
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
@@ -8,8 +9,11 @@ namespace CryptoExchange.Net.Objects;
/// <summary>
/// Call result
/// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record CallResult : ICallResult
{
private string DebugView => Success ? "Success" : $"Error: {Error}";
private static CallResult _successResult = new CallResult();
/// <inheritdoc />
@@ -1,6 +1,7 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
@@ -12,8 +13,11 @@ namespace CryptoExchange.Net.Objects;
/// <summary>
/// HTTP call result
/// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record HttpResult : IHttpResult
{
private string DebugView => $"[Req {RequestId}] " + (Success ? "Success" : $"Error: {Error}");
/// <summary>
/// Create a new success HTTP result
/// </summary>
@@ -251,8 +255,32 @@ public record HttpResult : IHttpResult
/// <inheritdoc />
[DebuggerDisplay("{DebugView,nq}")]
public record HttpResult<T> : HttpResult, IHttpResult<T>
{
private string DebugView
{
get
{
var result = new StringBuilder($"[Req {RequestId}] " + (Success ? "Success" : $"Error: {Error}"));
if (Data != null)
{
result.Append(", ");
var typeName = typeof(T).Name;
if (Data is Array ar)
{
result.Append($"{ar.Length} {typeName.Substring(0, typeName.Length - 2)}");
}
else
{
result.Append(typeName);
}
}
return result.ToString();
}
}
/// <summary>
/// ctor
/// </summary>
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
@@ -8,8 +9,11 @@ namespace CryptoExchange.Net.Objects;
/// <summary>
/// WebSocket call result
/// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record WebSocketResult : IWebSocketResult
{
private string DebugView => $"[Sckt {ConnectionId}] " + (RequestId == null ? "" : $"[Req {RequestId}] ") + (Success ? "Success" : $"Error: {Error}");
/// <summary>
/// ctor
/// </summary>
@@ -0,0 +1,209 @@
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects.Sockets
{
/// <summary>
/// Controller for an update subscription which isn't backed by a websocket connection. Can be used for testing.
/// </summary>
public class ManualUpdateSubscription
{
private readonly Func<Task> _closeAsync;
private readonly Func<Task> _reconnectAsync;
private readonly Func<Task<CallResult>> _resubscribeAsync;
private readonly ManualSubscription _manualSubscription;
private int _closedEventInvoked;
/// <summary>
/// The update subscription
/// </summary>
public UpdateSubscription Subscription { get; }
/// <summary>
/// The virtual socket id
/// </summary>
public int SocketId { get; }
/// <summary>
/// The last timestamp anything was received by the subscription
/// </summary>
public DateTime? LastReceiveTime { get; private set; }
/// <summary>
/// The current virtual websocket status
/// </summary>
public SocketStatus SocketStatus { get; private set; }
/// <summary>
/// Create a manually controlled update subscription
/// </summary>
/// <param name="socketId">The virtual socket id</param>
/// <param name="closeAsync">Callback when the subscription is closed</param>
/// <param name="reconnectAsync">Callback when a reconnect is requested</param>
/// <param name="resubscribeAsync">Callback when a resubscribe is requested</param>
public ManualUpdateSubscription(
int socketId = 0,
Func<Task>? closeAsync = null,
Func<Task>? reconnectAsync = null,
Func<Task<CallResult>>? resubscribeAsync = null)
{
SocketId = socketId;
SocketStatus = SocketStatus.Connected;
_closeAsync = closeAsync ?? (() => Task.CompletedTask);
_reconnectAsync = reconnectAsync ?? (() => Task.CompletedTask);
_resubscribeAsync = resubscribeAsync ?? (() => Task.FromResult(CallResult.Ok()));
_manualSubscription = new ManualSubscription();
_manualSubscription.Status = SubscriptionStatus.Subscribed;
Subscription = new UpdateSubscription(this, _manualSubscription);
}
/// <summary>
/// Set the last timestamp anything was received by the subscription
/// </summary>
/// <param name="timestamp">The receive timestamp</param>
public void SetLastReceiveTime(DateTime? timestamp)
{
LastReceiveTime = timestamp;
}
/// <summary>
/// Set the virtual websocket status
/// </summary>
/// <param name="status">The status</param>
public void SetSocketStatus(SocketStatus status)
{
SocketStatus = status;
}
/// <summary>
/// Set the subscription status
/// </summary>
/// <param name="status">The status</param>
public void SetSubscriptionStatus(SubscriptionStatus status)
{
_manualSubscription.Status = status;
}
/// <summary>
/// Invoke the connection lost event
/// </summary>
public void InvokeConnectionLost()
{
Subscription.HandleConnectionLostEvent();
}
/// <summary>
/// Invoke the connection restored event
/// </summary>
/// <param name="disconnectedPeriod">The period the connection was disconnected</param>
public void InvokeConnectionRestored(TimeSpan disconnectedPeriod)
{
Subscription.HandleConnectionRestoredEvent(disconnectedPeriod);
}
/// <summary>
/// Invoke the connection closed event
/// </summary>
public void InvokeConnectionClosed()
{
if (Interlocked.Exchange(ref _closedEventInvoked, 1) != 0)
return;
SocketStatus = SocketStatus.Closed;
_manualSubscription.Status = SubscriptionStatus.Closed;
Subscription.HandleConnectionClosedEvent();
}
/// <summary>
/// Invoke the resubscribing failed event
/// </summary>
/// <param name="error">The resubscribe error</param>
public void InvokeResubscribingFailed(Error error)
{
if (error == null)
throw new ArgumentNullException(nameof(error));
Subscription.HandleResubscribeFailedEvent(error);
}
/// <summary>
/// Invoke the activity paused event
/// </summary>
public void InvokeActivityPaused()
{
Subscription.HandlePausedEvent();
}
/// <summary>
/// Invoke the activity unpaused event
/// </summary>
public void InvokeActivityUnpaused()
{
Subscription.HandleUnpausedEvent();
}
/// <summary>
/// Invoke the exception event
/// </summary>
/// <param name="exception">The exception</param>
public void InvokeException(Exception exception)
{
if (exception == null)
throw new ArgumentNullException(nameof(exception));
_manualSubscription.InvokeExceptionHandler(exception);
}
internal async Task CloseAsync()
{
if (_manualSubscription.Status == SubscriptionStatus.Closed
|| _manualSubscription.Status == SubscriptionStatus.Closing)
return;
_manualSubscription.Status = SubscriptionStatus.Closing;
try
{
await _closeAsync().ConfigureAwait(false);
}
finally
{
_manualSubscription.Status = SubscriptionStatus.Closed;
}
}
internal Task ReconnectAsync()
{
return _reconnectAsync();
}
internal Task<CallResult> ResubscribeAsync()
{
return _resubscribeAsync();
}
private class ManualSubscription : Subscription
{
public ManualSubscription()
: base(NullLogger.Instance, false)
{
MessageRouter = MessageRouter.Create();
}
protected override Query? GetSubQuery(SocketConnection connection)
{
return null;
}
protected override Query? GetUnsubQuery(SocketConnection connection)
{
return null;
}
}
}
}
@@ -12,7 +12,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public class UpdateSubscription
{
private readonly SocketConnection _connection;
private readonly SocketConnection? _connection;
private readonly ManualUpdateSubscription? _manualSubscription;
internal readonly Subscription _subscription;
#if NET9_0_OR_GREATER
@@ -102,7 +103,7 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary>
/// The id of the socket
/// </summary>
public int SocketId => _connection.SocketId;
public int SocketId => _connection?.SocketId ?? _manualSubscription!.SocketId;
/// <summary>
/// The id of the subscription
@@ -112,12 +113,12 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary>
/// The last timestamp anything was received from the server
/// </summary>
public DateTime? LastReceiveTime => _connection.LastReceiveTime;
public DateTime? LastReceiveTime => _connection?.LastReceiveTime ?? _manualSubscription!.LastReceiveTime;
/// <summary>
/// The current websocket status
/// </summary>
public SocketStatus SocketStatus => _connection.Status;
public SocketStatus SocketStatus => _connection?.Status ?? _manualSubscription!.SocketStatus;
/// <summary>
/// The current subscription status
@@ -143,6 +144,18 @@ namespace CryptoExchange.Net.Objects.Sockets
_subscription.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
}
/// <summary>
/// ctor
/// </summary>
/// <param name="manualSubscription">The manual subscription for controlling events and data</param>
/// <param name="subscription">The subscription</param>
internal UpdateSubscription(ManualUpdateSubscription manualSubscription, Subscription subscription)
{
_manualSubscription = manualSubscription;
_subscription = subscription;
_subscription.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
}
private void UnsubscribeConnectionEvents()
{
lock (_eventLock)
@@ -150,22 +163,26 @@ namespace CryptoExchange.Net.Objects.Sockets
if (!_connectionEventsSubscribed)
return;
if (_connection != null)
{
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
_connection.ConnectionLost -= HandleConnectionLostEvent;
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
_connection.ActivityPaused -= HandlePausedEvent;
_connection.ActivityUnpaused -= HandleUnpausedEvent;
}
_connectionEventsSubscribed = false;
}
}
private void HandleConnectionClosedEvent()
internal void HandleConnectionClosedEvent()
{
UnsubscribeConnectionEvents();
// If we're not the subscription closing this connection don't bother emitting
if (!_subscription.IsClosingConnection)
if (_connection != null && !_subscription.IsClosingConnection)
return;
List<Action> handlers;
@@ -176,7 +193,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback();
}
private void HandleConnectionLostEvent()
internal void HandleConnectionLostEvent()
{
if (!_subscription.Active)
{
@@ -192,7 +209,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback();
}
private void HandleConnectionRestoredEvent(TimeSpan period)
internal void HandleConnectionRestoredEvent(TimeSpan period)
{
if (!_subscription.Active)
{
@@ -208,7 +225,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback(period);
}
private void HandleResubscribeFailedEvent(Error error)
internal void HandleResubscribeFailedEvent(Error error)
{
if (!_subscription.Active)
{
@@ -224,7 +241,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback(error);
}
private void HandlePausedEvent()
internal void HandlePausedEvent()
{
if (!_subscription.Active)
{
@@ -240,7 +257,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback();
}
private void HandleUnpausedEvent()
internal void HandleUnpausedEvent()
{
if (!_subscription.Active)
{
@@ -262,7 +279,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
public Task CloseAsync()
{
if (_connection != null)
return _connection.CloseAsync(_subscription);
return _manualSubscription!.CloseAsync();
}
/// <summary>
@@ -271,7 +291,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
public Task ReconnectAsync()
{
if (_connection != null)
return _connection.TriggerReconnectAsync();
return _manualSubscription!.ReconnectAsync();
}
/// <summary>
@@ -279,8 +302,14 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
/// <returns></returns>
internal async Task UnsubscribeAsync()
{
if (_connection != null)
{
await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false);
return;
}
await _manualSubscription!.CloseAsync().ConfigureAwait(false);
}
/// <summary>
@@ -289,7 +318,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
internal async Task<CallResult> ResubscribeAsync()
{
if (_connection != null)
return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false);
return await _manualSubscription!.ResubscribeAsync().ConfigureAwait(false);
}
}
}
@@ -24,7 +24,24 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// The volume in the last 24h
/// </summary>
public decimal Volume { get; set; }
public SharedOrderQuantity Volumes { get; set; }
private decimal? _volume;
/// <summary>
/// The volume in the last 24h
/// </summary>
[Obsolete("Use `Volumes` instead")]
public decimal Volume
{
get
{
if (_volume.HasValue)
return _volume.Value;
return Volumes.QuantityInBaseAsset ?? Volumes.QuantityInContracts ?? 0;
}
set => _volume = value;
}
/// <summary>
/// Change percentage in the last 24h
/// </summary>
@@ -49,13 +66,20 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// ctor
/// </summary>
public SharedFuturesTicker(SharedSymbol? sharedSymbol, string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage)
public SharedFuturesTicker(
SharedSymbol? sharedSymbol,
string symbol,
decimal? lastPrice,
decimal? highPrice,
decimal? lowPrice,
SharedOrderQuantity volumes,
decimal? changePercentage)
:base(sharedSymbol, symbol)
{
LastPrice = lastPrice;
HighPrice = highPrice;
LowPrice = lowPrice;
Volume = volume;
Volumes = volumes;
ChangePercentage = changePercentage;
}
}
@@ -6,7 +6,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Kline info
/// </summary>
[DebuggerDisplay("[{OpenTime}] O: {OpenPrice} H: {HighPrice} L: {LowPrice} C: {ClosePrice} V: {Volume}")]
[DebuggerDisplay("[{OpenTime}] O: {OpenPrice} H: {HighPrice} L: {LowPrice} C: {ClosePrice} V: {Volumes}")]
public record SharedKline : SharedSymbolModel
{
/// <summary>
@@ -29,15 +29,40 @@ namespace CryptoExchange.Net.SharedApis
/// Open price
/// </summary>
public decimal OpenPrice { get; set; }
private decimal? _volume;
/// <summary>
/// Volume in the base asset
/// </summary>
public decimal Volume { get; set; }
[Obsolete("Use `Volumes` instead")]
public decimal Volume
{
get
{
if (_volume.HasValue)
return _volume.Value;
return Volumes.QuantityInBaseAsset ?? Volumes.QuantityInContracts ?? 0;
}
set => _volume = value;
}
/// <summary>
/// The volume in the last 24h
/// </summary>
public SharedOrderQuantity Volumes { get; set; }
/// <summary>
/// ctor
/// </summary>
public SharedKline(SharedSymbol? sharedSymbol, string symbol, DateTime openTime, decimal closePrice, decimal highPrice, decimal lowPrice, decimal openPrice, decimal volume)
public SharedKline(
SharedSymbol? sharedSymbol,
string symbol,
DateTime openTime,
decimal closePrice,
decimal highPrice,
decimal lowPrice,
decimal openPrice,
SharedOrderQuantity volumes)
: base(sharedSymbol, symbol)
{
OpenTime = openTime;
@@ -45,7 +70,7 @@ namespace CryptoExchange.Net.SharedApis
HighPrice = highPrice;
LowPrice = lowPrice;
OpenPrice = openPrice;
Volume = volume;
Volumes = volumes;
}
}
}
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis
{
@@ -21,13 +22,31 @@ namespace CryptoExchange.Net.SharedApis
/// </summary>
public decimal? LowPrice { get; set; }
/// <summary>
/// The volume in the last 24h
/// </summary>
public SharedOrderQuantity Volumes { get; set; }
private decimal? _volume;
/// <summary>
/// Trade volume in base asset in the last 24h
/// </summary>
public decimal Volume { get; set; }
[Obsolete("Use `Volumes` instead")]
public decimal Volume
{
get
{
if (_volume.HasValue)
return _volume.Value;
return Volumes.QuantityInBaseAsset ?? Volumes.QuantityInContracts ?? 0;
}
set => _volume = value;
}
/// <summary>
/// Trade volume in quote asset in the last 24h
/// </summary>
public decimal? QuoteVolume { get; set; }
[Obsolete("Use `Volumes` instead")]
public decimal? QuoteVolume => Volumes?.QuantityInQuoteAsset;
/// <summary>
/// Change percentage in the last 24h
/// </summary>
@@ -36,13 +55,20 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// ctor
/// </summary>
public SharedSpotTicker(SharedSymbol? sharedSymbol, string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage)
public SharedSpotTicker(
SharedSymbol? sharedSymbol,
string symbol,
decimal? lastPrice,
decimal? highPrice,
decimal? lowPrice,
SharedOrderQuantity volumes,
decimal? changePercentage)
: base(sharedSymbol, symbol)
{
LastPrice = lastPrice;
HighPrice = highPrice;
LowPrice = lowPrice;
Volume = volume;
Volumes = volumes;
ChangePercentage = changePercentage;
}
}
@@ -12,7 +12,12 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Quantity of the trade
/// </summary>
public decimal Quantity { get; set; }
[Obsolete("Use `Quantities` instead")]
public decimal Quantity => Quantities.QuantityInBaseAsset ?? Quantities.QuantityInContracts ?? 0;
/// <summary>
/// The quantities of the trade
/// </summary>
public SharedOrderQuantity Quantities { get; set; }
/// <summary>
/// Price of the trade
/// </summary>
@@ -29,9 +34,9 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// ctor
/// </summary>
public SharedTrade(SharedSymbol? sharedSymbol, string symbol, decimal quantity, decimal price, DateTime timestamp) : base(sharedSymbol, symbol)
public SharedTrade(SharedSymbol? sharedSymbol, string symbol, SharedOrderQuantity quantities, decimal price, DateTime timestamp) : base(sharedSymbol, symbol)
{
Quantity = quantity;
Quantities = quantities;
Price = price;
Timestamp = timestamp;
}
@@ -142,6 +142,11 @@ namespace CryptoExchange.Net.SharedApis
[JsonConverter(typeof(SharedOrderQuantityConverter))]
public record SharedOrderQuantity : SharedQuantityReference
{
/// <summary>
/// The average price based on the base and quote asset quantities
/// </summary>
public decimal? AveragePrice => QuantityInBaseAsset == 0 ? null : QuantityInQuoteAsset / QuantityInBaseAsset;
/// <summary>
/// ctor
/// </summary>
@@ -155,6 +160,22 @@ namespace CryptoExchange.Net.SharedApis
{
}
/// <summary>
/// Get the quantity in quote asset. Will use the set `QuantityInQuoteAsset` property if it has a value, or `QuantityInBaseAsset` * `price` if not. Null otherwise.
/// </summary>
/// <param name="price">The price to use for the QuantityInBaseAsset to quote asset quantity calculation</param>
/// <returns>Quantity in quote asset if it's available or can be calculated, null otherwise</returns>
public decimal? GetQuantityInQuoteAsset(decimal? price)
{
if (QuantityInQuoteAsset != null)
return QuantityInQuoteAsset;
if (QuantityInBaseAsset != null && price != null)
return QuantityInBaseAsset * price;
return null;
}
/// <inheritdoc />
public override string ToString() => base.ToString();
}
@@ -607,7 +607,7 @@ namespace CryptoExchange.Net.Sockets.Default
SocketId,
typeIdentifier,
topicFilter!,
string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Where(x => x.TypeIdentifier == typeIdentifier).Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]")))));
string.Join(",", _listeners.SelectMany(x => x.MessageRouter.Routes.Where(x => x.TypeIdentifier == typeIdentifier).Select(x => x.TopicFilter ?? "[null]"))));
}
}
}
@@ -142,11 +142,16 @@ namespace CryptoExchange.Net.Testing
var issues = SystemTextJsonComparer.CompareData(expressionBody.Method.Name, data, originalData, compareNestedProperty, ignoreProperties, useSingleArrayItem ?? false);
foreach(var issue in issues)
{
if (issue is MissingPropertyException && !warnings?.Any(x => x.Message == issue.Message) == true)
if (issue is MissingPropertyException)
{
if (!warnings?.Any(x => x.Message == issue.Message) == true)
warnings?.Add(issue);
}
else
{
errors.Add(issue);
}
}
if (errors.Count > 0)
throw new AggregateException(errors);
@@ -284,8 +284,10 @@ namespace CryptoExchange.Net.Trackers.Klines
LastOpenTime = klines.Last().OpenTime,
HighPrice = klines.Select(d => d.LowPrice).Max(),
LowPrice = klines.Select(d => d.HighPrice).Min(),
#pragma warning disable CS0618 // Type or member is obsolete | Temporary to maintain previous behavior
Volume = klines.Select(d => d.Volume).Sum(),
AverageVolume = Math.Round(klines.OrderByDescending(d => d.OpenTime).Skip(1).Select(d => d.Volume).DefaultIfEmpty().Average(), 8)
#pragma warning restore
};
}
@@ -138,6 +138,11 @@ namespace CryptoExchange.Net.Trackers.Trades
}
}
/// <summary>
/// The type of quantity the trades and stats are denoted in
/// </summary>
public TradeQuantityType QuantityType { get; }
/// <inheritdoc />
public event Func<SharedTrade, Task>? OnAdded;
/// <inheritdoc />
@@ -156,12 +161,14 @@ namespace CryptoExchange.Net.Trackers.Trades
SharedSymbol symbol,
int? limit = null,
TimeSpan? period = null,
TradeQuantityType tradeQuantityType = TradeQuantityType.BaseAsset,
ExchangeParameters? exchangeParameters = null)
{
_logger = logger ?? new NullLogger<TradeTracker>();
_recentRestClient = recentRestClient;
_historyRestClient = historyRestClient;
_socketClient = socketClient;
QuantityType = tradeQuantityType;
_exchangeParameters = exchangeParameters;
Exchange = socketClient.Exchange;
Symbol = symbol;
@@ -170,22 +177,41 @@ namespace CryptoExchange.Net.Trackers.Trades
Period = period;
}
private static TradesStats GetStats(IEnumerable<SharedTrade> trades)
private TradesStats GetStats(IEnumerable<SharedTrade> trades)
{
if (!trades.Any())
return new TradesStats();
return new TradesStats
var stats = new TradesStats
{
TradeCount = trades.Count(),
FirstTradeTime = trades.First().Timestamp,
LastTradeTime = trades.Last().Timestamp,
AveragePrice = Math.Round(trades.Select(d => d.Price).DefaultIfEmpty().Average(), 8),
VolumeWeightedAveragePrice = trades.Any() ? Math.Round(trades.Select(d => d.Price * d.Quantity).DefaultIfEmpty().Sum() / trades.Select(d => d.Quantity).DefaultIfEmpty().Sum(), 8) : null,
Volume = Math.Round(trades.Sum(d => d.Quantity), 8),
QuoteVolume = Math.Round(trades.Sum(d => d.Quantity * d.Price), 8),
BuySellRatio = Math.Round(trades.Where(x => x.Side == SharedOrderSide.Buy).Sum(x => x.Quantity) / trades.Sum(x => x.Quantity), 8)
QuoteVolume = Math.Round(trades.Sum(d => d.Quantities.GetQuantityInQuoteAsset(d.Price) ?? 0), 8),
};
if (QuantityType == TradeQuantityType.BaseAsset)
{
stats.VolumeWeightedAveragePrice =
trades.Any()
? Math.Round(trades.Select(d => d.Quantities.GetQuantityInQuoteAsset(d.Price) ?? 0).DefaultIfEmpty().Sum() / trades.Select(d => d.Quantities.QuantityInBaseAsset!.Value).DefaultIfEmpty().Sum(), 8)
: null;
stats.Volume = Math.Round(trades.Sum(d => d.Quantities.QuantityInBaseAsset!.Value), 8);
stats.BuySellRatio = Math.Round(trades.Where(x => x.Side == SharedOrderSide.Buy).Sum(x => x.Quantities.QuantityInBaseAsset!.Value) / trades.Sum(x => x.Quantities.QuantityInBaseAsset!.Value), 8);
}
else
{
stats.VolumeWeightedAveragePrice =
trades.Any()
? Math.Round(trades.Select(d => d.Quantities.GetQuantityInQuoteAsset(d.Price) ?? 0).DefaultIfEmpty().Sum() / trades.Select(d => d.Quantities.QuantityInContracts!.Value).DefaultIfEmpty().Sum(), 8)
: null;
stats.Volume = Math.Round(trades.Sum(d => d.Quantities.QuantityInContracts!.Value), 8);
stats.BuySellRatio = Math.Round(trades.Where(x => x.Side == SharedOrderSide.Buy).Sum(x => x.Quantities.QuantityInContracts!.Value) / trades.Sum(x => x.Quantities.QuantityInContracts!.Value), 8);
}
return stats;
}
/// <inheritdoc />
@@ -498,4 +524,19 @@ namespace CryptoExchange.Net.Trackers.Trades
Status = SyncStatus.Synced;
}
}
/// <summary>
/// The quantities to use for trade tracking
/// </summary>
public enum TradeQuantityType
{
/// <summary>
/// Base asset
/// </summary>
BaseAsset,
/// <summary>
/// Contracts
/// </summary>
Contracts
}
}
+29 -27
View File
@@ -5,34 +5,36 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="13.0.0" />
<PackageReference Include="Bitfinex.Net" Version="11.0.0" />
<PackageReference Include="BitMart.Net" Version="4.0.0" />
<PackageReference Include="BloFin.Net" Version="3.0.0" />
<PackageReference Include="Bybit.Net" Version="7.0.0" />
<PackageReference Include="CoinEx.Net" Version="11.0.0" />
<PackageReference Include="CoinW.Net" Version="3.0.0" />
<PackageReference Include="CryptoCom.Net" Version="4.0.0" />
<PackageReference Include="DeepCoin.Net" Version="4.0.0" />
<PackageReference Include="GateIo.Net" Version="4.0.0" />
<PackageReference Include="HyperLiquid.Net" Version="5.0.0" />
<PackageReference Include="JK.BingX.Net" Version="4.0.0" />
<PackageReference Include="JK.Bitget.Net" Version="4.0.0" />
<PackageReference Include="JK.Mexc.Net" Version="6.0.0" />
<PackageReference Include="JK.OKX.Net" Version="5.0.1" />
<PackageReference Include="Jkorf.Aster.Net" Version="4.0.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="4.0.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="4.0.0" />
<PackageReference Include="JKorf.HTX.Net" Version="9.0.0" />
<PackageReference Include="JKorf.Lighter.Net" Version="1.0.0" />
<PackageReference Include="JKorf.Upbit.Net" Version="3.0.0" />
<PackageReference Include="KrakenExchange.Net" Version="8.0.0" />
<PackageReference Include="Kucoin.Net" Version="9.0.0" />
<PackageReference Include="Binance.Net" Version="13.3.0" />
<PackageReference Include="Bitfinex.Net" Version="11.3.0" />
<PackageReference Include="BitMart.Net" Version="4.3.0" />
<PackageReference Include="BloFin.Net" Version="3.3.0" />
<PackageReference Include="Bybit.Net" Version="7.3.0" />
<PackageReference Include="CoinEx.Net" Version="11.3.0" />
<PackageReference Include="CoinW.Net" Version="3.3.0" />
<PackageReference Include="CryptoCom.Net" Version="4.3.0" />
<PackageReference Include="DeepCoin.Net" Version="4.3.0" />
<PackageReference Include="GateIo.Net" Version="4.4.0" />
<PackageReference Include="HyperLiquid.Net" Version="5.4.0" />
<PackageReference Include="JK.BingX.Net" Version="4.3.0" />
<PackageReference Include="JK.Bitget.Net" Version="4.3.2" />
<PackageReference Include="JK.Mexc.Net" Version="6.4.0" />
<PackageReference Include="JK.OKX.Net" Version="5.3.1" />
<PackageReference Include="Jkorf.Aster.Net" Version="4.3.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="4.3.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="4.4.0" />
<PackageReference Include="JKorf.HTX.Net" Version="9.3.0" />
<PackageReference Include="JKorf.Lighter.Net" Version="1.4.0" />
<PackageReference Include="JKorf.Upbit.Net" Version="3.3.0" />
<PackageReference Include="KrakenExchange.Net" Version="8.3.0" />
<PackageReference Include="Kucoin.Net" Version="9.3.0" />
<PackageReference Include="LBank.Net" Version="1.0.0" />
<PackageReference Include="Pionex.Net" Version="1.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Toobit.Net" Version="4.0.0" />
<PackageReference Include="Weex.Net" Version="2.0.0" />
<PackageReference Include="WhiteBit.Net" Version="4.0.0" />
<PackageReference Include="XT.Net" Version="4.0.0" />
<PackageReference Include="Toobit.Net" Version="4.3.0" />
<PackageReference Include="Weex.Net" Version="2.3.0" />
<PackageReference Include="WhiteBit.Net" Version="4.3.0" />
<PackageReference Include="XT.Net" Version="4.3.0" />
</ItemGroup>
</Project>
+11 -1
View File
@@ -18,9 +18,11 @@
@inject IHyperLiquidRestClient hyperLiquidClient
@inject IKrakenRestClient krakenClient
@inject IKucoinRestClient kucoinClient
@inject ILBankRestClient lbankClient
@inject ILighterRestClient lighterClient
@inject IMexcRestClient mexcClient
@inject IOKXRestClient okxClient
@inject IPionexRestClient pionexClient
@inject IToobitRestClient toobitClient
@inject IUpbitRestClient upbitClient
@inject IWeexRestClient weexClient
@@ -57,16 +59,18 @@
var hyperLiquidTask = hyperLiquidClient.FuturesApi.ExchangeData.GetExchangeInfoAndTickersAsync();
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var lBankTask = lbankClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
var lighterTask = lighterClient.ExchangeApi.ExchangeData.GetSymbolDetailsAsync("BTC");
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTC-USDT");
var pionexTask = pionexClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT");
var toobitTask = toobitClient.SpotApi.ExchangeData.GetTickersAsync("BTCUSDT");
var upbitTask = upbitClient.SpotApi.ExchangeData.GetTickerAsync("USDT-BTC");
var weexTask = weexClient.SpotApi.ExchangeData.GetTickersAsync(["BTCUSDT"]);
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
await Task.WhenAll(asterTask, binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bloFinTask, bitmexTask, bybitTask, coinexTask, coinWTask, 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, lBankTask, mexcTask, okxTask, pionexTask);
if (asterTask.Result.Success)
_prices.Add("Aster", asterTask.Result.Data.LastPrice);
@@ -133,6 +137,9 @@
if (kucoinTask.Result.Success)
_prices.Add("Kucoin", kucoinTask.Result.Data.LastPrice ?? 0);
if (lBankTask.Result.Success)
_prices.Add("LBank", lBankTask.Result.Data.Single().Ticker.LastPrice);
if (lighterTask.Result.Success)
_prices.Add("Lighter", lighterTask.Result.Data.PerpSymbols[0].LastPrice);
@@ -142,6 +149,9 @@
if (okxTask.Result.Success)
_prices.Add("OKX", okxTask.Result.Data.LastPrice ?? 0);
if (pionexTask.Result.Success)
_prices.Add("Pionex", pionexTask.Result.Data.Single().ClosePrice);
if (toobitTask.Result.Success)
_prices.Add("Toobit", toobitTask.Result.Data.Single().LastPrice ?? 0);
+4 -1
View File
@@ -18,9 +18,11 @@
@inject IHyperLiquidSocketClient hyperLiquidSocketClient
@inject IKrakenSocketClient krakenSocketClient
@inject IKucoinSocketClient kucoinSocketClient
@inject ILBankSocketClient lBankSocketClient
@inject ILighterSocketClient lighterSocketClient
@inject IMexcSocketClient mexcSocketClient
@inject IOKXSocketClient okxSocketClient
@inject IPionexSocketClient pionexSocketClient
@inject IToobitSocketClient toobitSocketClient
@inject IUpbitSocketClient upbitSocketClient
@inject IWeexSocketClient weexSocketClient
@@ -65,11 +67,11 @@
deepCoinSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH-BTC", data => UpdateData("DeepCoin", data.Data.LastPrice ?? 0)),
gateioSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("GateIo", data.Data.LastPrice)),
htxSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("HTX", data.Data.ClosePrice ?? 0)),
xtSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("eth_btc", data => UpdateData("XT", data.Data.LastPrice ?? 0)),
// HyperLiquid doesn't support the ETH/BTC pair
//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)),
lBankSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("eth_btc", data => UpdateData("LBank", 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)),
@@ -77,6 +79,7 @@
//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)),
xtSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("eth_btc", data => UpdateData("XT", data.Data.LastPrice ?? 0)),
};
await Task.WhenAll(tasks);
@@ -24,9 +24,11 @@
@using Kucoin.Net
@using Kucoin.Net.Clients
@using Kucoin.Net.Interfaces
@using LBank.Net.Interfaces
@using Lighter.Net.Interfaces
@using Mexc.Net.Interfaces
@using OKX.Net.Interfaces;
@using Pionex.Net.Interfaces;
@using Upbit.Net.Interfaces;
@using Toobit.Net.Interfaces;
@using Weex.Net.Interfaces
@@ -51,9 +53,11 @@
@inject IHyperLiquidOrderBookFactory hyperLiquidFactory
@inject IKrakenOrderBookFactory krakenFactory
@inject IKucoinOrderBookFactory kucoinFactory
@inject ILBankOrderBookFactory lBankFactory
@inject ILighterOrderBookFactory lighterFactory
@inject IMexcOrderBookFactory mexcFactory
@inject IOKXOrderBookFactory okxFactory
@inject IPionexOrderBookFactory pionexFactory
@inject IToobitOrderBookFactory toobitFactory
@inject IUpbitOrderBookFactory upbitFactory
@inject IWeexOrderBookFactory weexFactory
@@ -112,9 +116,11 @@
{ "HyperLiquid", hyperLiquidFactory.Create("UETH/USDC") },
{ "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
{ "LBank", lBankFactory.CreateSpot("eth_usdt") },
{ "Lighter", lighterFactory.Create("ETH/USDC") },
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") },
{ "OKX", okxFactory.Create("ETH-BTC") },
{ "Pionex", pionexFactory.CreateSpot("ETH_USDT") },
{ "Toobit", toobitFactory.CreateSpot("ETHUSDT") },
{ "Upbit", upbitFactory.CreateSpot("BTC-ETH") },
{ "Weex", weexFactory.CreateSpot("ETHUSDT") },
@@ -24,9 +24,11 @@
@using Kraken.Net.Interfaces
@using Kucoin.Net.Clients
@using Kucoin.Net.Interfaces
@using LBank.Net.Interfaces
@using Lighter.Net.Interfaces
@using Mexc.Net.Interfaces
@using OKX.Net.Interfaces;
@using Pionex.Net.Interfaces;
@using Upbit.Net.Interfaces;
@using Toobit.Net.Interfaces;
@using Weex.Net.Interfaces
@@ -51,9 +53,11 @@
@inject IHyperLiquidTrackerFactory hyperLiquidFactory
@inject IKrakenTrackerFactory krakenFactory
@inject IKucoinTrackerFactory kucoinFactory
@inject ILBankTrackerFactory lBankFactory
@inject ILighterTrackerFactory lighterFactory
@inject IMexcTrackerFactory mexcFactory
@inject IOKXTrackerFactory okxFactory
@inject IPionexTrackerFactory pionexFactory
@inject IToobitTrackerFactory toobitFactory
@inject IUpbitTrackerFactory upbitFactory
@inject IWeexTrackerFactory weexFactory
@@ -105,9 +109,11 @@
{ hyperLiquidFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ krakenFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ kucoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ lBankFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ lighterFactory.CreateTradeTracker(futuresSymbol, period: TimeSpan.FromMinutes(5)) },
{ mexcFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ okxFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ pionexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ toobitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ upbitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ weexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
+2
View File
@@ -52,9 +52,11 @@ namespace BlazorClient
services.AddHTX();
services.AddKraken();
services.AddKucoin();
services.AddLBank();
services.AddLighter();
services.AddMexc();
services.AddOKX();
services.AddPionex();
services.AddToobit();
services.AddUpbit();
services.AddWeex();
+2
View File
@@ -27,9 +27,11 @@
@using HyperLiquid.Net.Interfaces.Clients;
@using Kraken.Net.Interfaces.Clients;
@using Kucoin.Net.Interfaces.Clients;
@using LBank.Net.Interfaces.Clients
@using Lighter.Net.Interfaces.Clients
@using Mexc.Net.Interfaces.Clients;
@using OKX.Net.Interfaces.Clients;
@using Pionex.Net.Interfaces.Clients;
@using Upbit.Net.Interfaces.Clients;
@using Toobit.Net.Interfaces.Clients;
@using Weex.Net.Interfaces.Clients
+9 -1189
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -2,12 +2,14 @@
> Base C#/.NET library for cryptocurrency exchange API client implementations. Provides a standardized abstraction (REST, WebSocket, authentication, rate limiting, error handling, order book management, shared cross-exchange interfaces) that 28+ exchange-specific libraries are built on top of.
CryptoExchange.Net itself is not used directly — install one of the exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.) or `CryptoClients.Net` to access all exchanges via a single bundle. The base library is what makes the entire ecosystem feel consistent: same `HttpResult<T>` REST result pattern, same `WebSocketResult<UpdateSubscription>` websocket subscription pattern, same DI registration, same shared interfaces across all exchanges. Current version: 12.3.0. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0. Native AOT supported.
CryptoExchange.Net itself is not used directly — install one of the exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.) or `CryptoClients.Net` to access all exchanges via a single bundle. The base library is what makes the entire ecosystem feel consistent: same `HttpResult<T>` REST result pattern, same `WebSocketResult<UpdateSubscription>` websocket subscription pattern, same DI registration, same shared interfaces across all exchanges. Current version: 12.4.0. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0. Native AOT supported.
The standout feature for cross-exchange code is `CryptoExchange.Net.SharedApis` — a set of interfaces (`ISpotTickerRestClient`, `ISpotOrderRestClient`, `IBalanceRestClient`, etc.) implemented by every exchange library. Same call signature works against any exchange.
Version 12.2.0 adds typed asset metadata to shared symbol discovery. `SharedSpotSymbol` and `SharedFuturesSymbol` expose `DisplayName` plus base/quote `SharedAssetType` and `SharedAssetSubType` values. `GetSymbolsRequest` can filter on those four type fields. After symbol discovery, `ISpotSymbolRestClient.SpotSymbolCatalog` and `IFuturesSymbolRestClient.FuturesSymbolCatalog` provide asset and symbol dictionaries; each catalog is available only after the corresponding `Get*SymbolsAsync` call. `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` are best-effort helpers for exchange-library implementations.
Version 12.4.0 represents market-data quantities with `SharedOrderQuantity`: use `Volumes` on `SharedSpotTicker`, `SharedFuturesTicker`, and `SharedKline`, and `Quantities` on `SharedTrade`. The old scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete. Exchange-library implementations must pass `SharedOrderQuantity` to these model constructors.
## Documentation
- [README](https://github.com/JKorf/CryptoExchange.Net/blob/master/README.md): Overview, full ecosystem table (28+ exchange libraries), installation per exchange, complete release notes
@@ -24,7 +26,7 @@ Version 12.2.0 adds typed asset metadata to shared symbol discovery. `SharedSpot
## Reference
- [Ecosystem libraries list](https://github.com/JKorf/CryptoExchange.Net#cryptoexchangenet-ecosystem): Aster, Binance, BingX, Bitfinex, Bitget, BitMart, BitMEX, Bitstamp, BloFin, Bybit, Coinbase, CoinEx, CoinW, CoinGecko, Crypto.com, DeepCoin, Gate.io, HTX, HyperLiquid, Kraken, Kucoin, Mexc, OKX, Polymarket, Toobit, Upbit, Weex, WhiteBit, XT
- [Ecosystem libraries list](https://github.com/JKorf/CryptoExchange.Net#cryptoexchangenet-ecosystem): Aster, Binance, BingX, Bitfinex, Bitget, BitMart, BitMEX, Bitstamp, BloFin, Bybit, Coinbase, CoinEx, CoinW, CoinGecko, Crypto.com, DeepCoin, Gate.io, HTX, HyperLiquid, Kraken, Kucoin, Mexc, OKX, Pionex, Polymarket, Toobit, Upbit, Weex, WhiteBit, XT
- [CryptoClients.Net](https://github.com/JKorf/CryptoClients.Net): Single bundle package for all exchange libraries
- [CryptoManager.Net](https://github.com/JKorf/CryptoManager.Net): Full demo application using CryptoClients.Net
- [NuGet Package](https://www.nuget.org/packages/CryptoExchange.Net): Latest stable release on NuGet