mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-11 16:32:57 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 860d753ad6 | |||
| e16c91792c | |||
| a3d95da9fa | |||
| 907399b878 | |||
| 9eab0d967e | |||
| 0dee68e8ae | |||
| d838d3377f | |||
| 2141ad9061 | |||
| 9be8798ccf | |||
| 4803ed91cd | |||
| 20bddd5c37 | |||
| 0e75ddb3d0 | |||
| 0e5b46002c | |||
| 8f7c71f9ce |
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:**
|
||||
|
||||
@@ -91,28 +91,25 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var evnt = new AsyncResetEvent(false, true);
|
||||
|
||||
var waiters = new List<Task<bool>>();
|
||||
for(var i = 0; i < 10; i++)
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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)";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
|
||||
_connection.ConnectionLost -= HandleConnectionLostEvent;
|
||||
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
|
||||
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
|
||||
_connection.ActivityPaused -= HandlePausedEvent;
|
||||
_connection.ActivityUnpaused -= HandleUnpausedEvent;
|
||||
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()
|
||||
{
|
||||
return _connection.CloseAsync(_subscription);
|
||||
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()
|
||||
{
|
||||
return _connection.TriggerReconnectAsync();
|
||||
if (_connection != null)
|
||||
return _connection.TriggerReconnectAsync();
|
||||
|
||||
return _manualSubscription!.ReconnectAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -280,7 +303,13 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// <returns></returns>
|
||||
internal async Task UnsubscribeAsync()
|
||||
{
|
||||
await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false);
|
||||
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()
|
||||
{
|
||||
return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false);
|
||||
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,10 +142,15 @@ 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)
|
||||
warnings?.Add(issue);
|
||||
if (issue is MissingPropertyException)
|
||||
{
|
||||
if (!warnings?.Any(x => x.Message == issue.Message) == true)
|
||||
warnings?.Add(issue);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(issue);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
|
||||
@@ -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)
|
||||
AveragePrice = Math.Round(trades.Select(d => d.Price).DefaultIfEmpty().Average(), 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user