1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-10-14 15:11:44 +00:00

Compare commits

..

13 Commits

Author SHA1 Message Date
Jkorf f079e6f0f4 Updated to version 13.2.0 2026-10-13 11:41:36 +02:00
Jkorf c6647f6815 Fixed DelayAfterConnect applying on failed connection attemps on HighPerf connections 2026-10-13 10:38:21 +02:00
Jkorf 5135047097 Fix for duplicate subscription coverage 2026-10-13 10:37:49 +02:00
Jkorf 4f25a6e7d6 Added processor parameter for some methods in SocketApiClient to allow specific overriding behavior 2026-10-13 10:08:16 +02:00
Jan Korf a0f3dcaa4c WebSocket covered subscriptions
Updated WebSocket duplicate subscription handling
Fixed incorrect status keeping of WebSocket subscriptions which don't expect a response
2026-10-12 13:19:44 +02:00
Jkorf 550afd41c3 Added DelayAfterConnect to SocketApiOptions, fixed DelayAfterConnect not being respected on reconnection 2026-10-02 15:42:35 +02:00
Jkorf afdced3bb8 Added Shared API ISubscribeFundingInfoSocket 2026-10-02 11:09:07 +02:00
Jkorf f8c657f308 Updated Shared APIs ISubscribeIncrementalOrderBook 2026-10-02 09:33:15 +02:00
Jkorf c9ae1bf379 Updated to version 13.1.0 2026-09-28 11:23:36 +02:00
Jkorf 59acbd648e Added UnsubscribeAllAsync method to Shared APIs ISharedSubscription and IShaerdApiClientBase 2026-09-28 11:11:41 +02:00
Jkorf 7a11bd0c5e Added WaitAllAsync extension method 2026-09-25 09:33:45 +02:00
Jkorf 9502db6d58 Added SubscribeAllAsync extension methods for Shared API resolved capabilities 2026-09-25 08:32:57 +02:00
Jkorf e6ab899654 Updated examples 2026-09-24 11:41:44 +02:00
31 changed files with 600 additions and 134 deletions
+72 -14
View File
@@ -286,7 +286,7 @@ namespace CryptoExchange.Net.Clients
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
if (!socketResult.Success)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, socketResult.Error);
@@ -309,7 +309,7 @@ namespace CryptoExchange.Net.Clients
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct, subscription).ConfigureAwait(false);
if (!connectResult.Success)
return WebSocketResult.Fail<UpdateSubscription>(Exchange, connectResult.Error!);
@@ -482,7 +482,7 @@ namespace CryptoExchange.Net.Clients
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct, processor: query).ConfigureAwait(false);
if (!socketResult.Success)
return QueryResult.Fail<THandlerResponse>(Exchange, socketResult.Error);
@@ -495,7 +495,7 @@ namespace CryptoExchange.Net.Clients
released = true;
}
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct, query).ConfigureAwait(false);
if (!connectResult.Success)
return QueryResult.Fail<THandlerResponse>(Exchange, connectResult.Error!);
}
@@ -524,7 +524,18 @@ namespace CryptoExchange.Net.Clients
/// <param name="authenticated">Whether the socket should authenticated</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
protected virtual Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
=> ConnectIfNeededAsync(socket, authenticated, ct, null);
/// <summary>
/// Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed
/// </summary>
/// <param name="socket">The connection to check</param>
/// <param name="authenticated">Whether the socket should authenticated</param>
/// <param name="processor">The message processor requesting a connection</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct, IMessageProcessor? processor)
{
if (socket.Connected)
return CallResult.Ok();
@@ -533,16 +544,13 @@ namespace CryptoExchange.Net.Clients
if (!connectResult.Success)
return connectResult;
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
if (!authenticated || socket.Authenticated)
return CallResult.Ok();
if (socket is not SocketConnection sc)
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
var result = await AuthenticateSocketAsync(sc, processor).ConfigureAwait(false);
if (!result.Success)
await socket.CloseAsync().ConfigureAwait(false);
@@ -554,13 +562,22 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="socket">Socket to authenticate</param>
/// <returns></returns>
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
public virtual Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
=> AuthenticateSocketAsync(socket, null);
/// <summary>
/// Authenticate a socket connection
/// </summary>
/// <param name="socket">Socket to authenticate</param>
/// <param name="processor">The message processor requesting authentication</param>
/// <returns></returns>
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket, IMessageProcessor? processor)
{
if (GetAuthenticationProvider() == null)
return CallResult.Fail(new NoApiCredentialsError());
_logger.AttemptingToAuthenticate(socket.SocketId);
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
var authRequest = await GetAuthenticationRequestAsync(socket, processor).ConfigureAwait(false);
if (authRequest != null)
{
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
@@ -581,13 +598,21 @@ namespace CryptoExchange.Net.Clients
return CallResult.Ok();
}
/// <summary>
/// Should return the request which can be used to authenticate a socket connection
/// </summary>
/// <returns></returns>
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
Task.FromResult(GetAuthenticationProvider()!.GetAuthenticationQuery(this, connection));
/// <summary>
/// Should return the request which can be used to authenticate a socket connection
/// </summary>
/// <returns></returns>
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection, IMessageProcessor? processor) =>
GetAuthenticationRequestAsync(connection);
/// <summary>
/// Adds a system subscription. Used for example to reply to ping requests
/// </summary>
@@ -645,6 +670,18 @@ namespace CryptoExchange.Net.Clients
&& (AllowTopicsOnTheSameConnection || !connection.Topics.Contains(topic));
}
/// <summary>
/// Whether the connection can be used for a new subscription or query with the provided parameters
/// </summary>
/// <param name="connection">The connection to check</param>
/// <param name="address">The address set by the request</param>
/// <param name="authenticated">Whether the request needs an authenticated connection</param>
/// <param name="processor">The processor requesting a connection</param>
/// <param name="topic">Topic of the request</param>
/// <returns>True if connection can be used</returns>
protected virtual bool ConnectionCanBeUsedFor(SocketConnection connection, string address, bool authenticated, IMessageProcessor? processor, string? topic = null)
=> ConnectionCanBeUsedFor(connection, address, authenticated, topic);
/// <summary>
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
/// </summary>
@@ -655,15 +692,36 @@ namespace CryptoExchange.Net.Clients
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
/// <param name="individualSubscriptionCount">The number of individual subscriptions in this subscribe request</param>
/// <returns></returns>
protected virtual Task<CallResult<SocketConnection>> GetSocketConnection(
string address,
bool authenticated,
bool dedicatedRequestConnection,
CancellationToken ct,
string? topic = null,
int individualSubscriptionCount = 1)
=> GetSocketConnection(address, authenticated, dedicatedRequestConnection, ct, null, topic, individualSubscriptionCount);
/// <summary>
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
/// </summary>
/// <param name="address">The address the socket is for</param>
/// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
/// <param name="ct">Cancellation token</param>
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
/// <param name="individualSubscriptionCount">The number of individual subscriptions in this subscribe request</param>
/// <param name="processor">The processor requesting a connection</param>
/// <returns></returns>
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(
string address,
bool authenticated,
bool dedicatedRequestConnection,
CancellationToken ct,
IMessageProcessor? processor,
string? topic = null,
int individualSubscriptionCount = 1)
{
var socketQuery = _socketConnections.Where(s => ConnectionCanBeUsedFor(s.Value, address, authenticated, topic)).Select(x => x.Value); // Don't ToList this so the query is executed again when called
var socketQuery = _socketConnections.Where(s => ConnectionCanBeUsedFor(s.Value, address, authenticated, processor, topic)).Select(x => x.Value); // Don't ToList this so the query is executed again when called
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
var delayStart = DateTime.UtcNow;
@@ -862,7 +920,7 @@ namespace CryptoExchange.Net.Clients
RateLimitAdmissionCallbackRequest = () => AdmissionOverride.Value,
Proxy = ClientOptions.Proxy,
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
ReceiveBufferSize = ClientOptions.ReceiveBufferSize
};
/// <summary>
+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>13.0.0</PackageVersion>
<AssemblyVersion>13.0.0</AssemblyVersion>
<FileVersion>13.0.0</FileVersion>
<PackageVersion>13.2.0</PackageVersion>
<AssemblyVersion>13.2.0</AssemblyVersion>
<FileVersion>13.2.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>
@@ -19,6 +19,11 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary>
public int? MaxSocketConnections { get; set; }
/// <summary>
/// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting.
/// </summary>
public TimeSpan? DelayAfterConnect { get; set; }
/// <summary>
/// Set the values of this options on the target options
/// </summary>
@@ -28,6 +33,7 @@ namespace CryptoExchange.Net.Objects.Options
item.SocketNoDataTimeout = SocketNoDataTimeout;
item.AutoTimestamp = AutoTimestamp;
item.MaxSocketConnections = MaxSocketConnections;
item.DelayAfterConnect = DelayAfterConnect;
return item;
}
}
@@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
/// Get user funding history capability.
/// </summary>
public static SharedRestCapability<IGetUserFundingHistory, IGetUserFundingHistoryRest> GetUserFundingHistory { get; } = new();
/// <summary>
/// Subscribe to funding info updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeFundingInfoSocket> SubscribeFundingInfo { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using CryptoExchange.Net.Interfaces;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Order book info
/// </summary>
public record SharedIncrementalOrderBook : SharedOrderBook
{
/// <summary>
/// The sequence number of the first book update in this update.
/// </summary>
public long? StartSequenceNumber { get; set; }
/// <summary>
/// ctor
/// </summary>
public SharedIncrementalOrderBook(SharedQuantityType quantityType, long? startSequenceNumber, long? endSequenceNumber, ISymbolOrderBookEntry[] asks, ISymbolOrderBookEntry[] bids)
:base(quantityType, endSequenceNumber, asks, bids)
{
StartSequenceNumber = startSequenceNumber;
}
}
}
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
{
@@ -104,6 +105,11 @@ namespace CryptoExchange.Net.SharedApis
SharedCapabilityResolution<T>? GetCapability<T>(SharedCapabilityReference<T> capability, TradingMode tradingMode)
where T : ISharedApiCapability;
/// <summary>
/// Unsubscribes all subscriptions created by the underlying socket API
/// client and closes its subscription connections.
/// </summary>
Task UnsubscribeAllAsync();
}
/// <summary>
@@ -209,6 +215,15 @@ namespace CryptoExchange.Net.SharedApis
return GetCapabilities<T>(tradingMode);
}
/// <inheritdoc />
public Task UnsubscribeAllAsync()
{
return Task.WhenAll(
_sharedApis
.OfType<ISharedSubscription>()
.Select(x => x.UnsubscribeAllAsync()));
}
object? ISharedApiClientResolver.GetCapability(Type capabilityType)
{
return _sharedApis
@@ -1,10 +1,12 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
{
@@ -624,5 +626,212 @@ namespace CryptoExchange.Net.SharedApis
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to trade updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeTradesSocket>> capabilities,
SubscribeTradeRequest request,
Action<DataEvent<SharedTrade[]>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToTradeUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to balance updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeBalancesSocket>> capabilities,
SubscribeBalancesRequest request,
Action<DataEvent<SharedBalance[]>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToBalanceUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to index price updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeIndexPriceSocket>> capabilities,
SubscribeIndexPriceRequest request,
Action<DataEvent<SharedIndexPrice>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToIndexPriceUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to funding info updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeFundingInfoSocket>> capabilities,
SubscribeFundingInfoRequest request,
Action<DataEvent<SharedFundingInfo>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToFundingInfoUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to kline updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeKlinesSocket>> capabilities,
SubscribeKlineRequest request,
Action<DataEvent<SharedKline>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToKlineUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to mark price updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeMarkPriceSocket>> capabilities,
SubscribeMarkPriceRequest request,
Action<DataEvent<SharedMarkPrice>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToMarkPriceUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to order book updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeOrderBookSocket>> capabilities,
SubscribeOrderBookRequest request,
Action<DataEvent<SharedOrderBook>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToOrderBookUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to incremental order book updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeIncrementalOrderBookSocket>> capabilities,
SubscribeOrderBookRequest request,
Action<DataEvent<SharedIncrementalOrderBook>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToIncrementalOrderBookUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to futures order updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeFuturesOrdersSocket>> capabilities,
SubscribeFuturesOrderRequest request,
Action<DataEvent<SharedFuturesOrderUpdate[]>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToFuturesOrderUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to spot order updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeSpotOrdersSocket>> capabilities,
SubscribeSpotOrderRequest request,
Action<DataEvent<SharedSpotOrderUpdate[]>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToSpotOrderUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to position updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribePositionsSocket>> capabilities,
SubscribePositionRequest request,
Action<DataEvent<SharedPosition[]>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToPositionUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to ticker updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeTickerSocket>> capabilities,
SubscribeTickerRequest request,
Action<DataEvent<SharedTicker>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToTickerUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to all ticker updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeAllTickersSocket>> capabilities,
SubscribeAllTickersRequest request,
Action<DataEvent<SharedTicker[]>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToAllTickersUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Subscribe to user trade updates for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
this IEnumerable<SharedCapabilityResolution<ISubscribeUserTradesSocket>> capabilities,
SubscribeUserTradeRequest request,
Action<DataEvent<SharedUserTrade[]>> onData,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.SubscribeToUserTradeUpdatesAsync(request, onData, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Wait for all results from an IAsyncEnumerable and return them as an array
/// </summary>
public static async Task<T[]> WaitAllAsync<T>(this IAsyncEnumerable<T> enumerable)
{
var result = new List<T>();
await foreach (var itemResult in enumerable.ConfigureAwait(false))
result.Add(itemResult);
return result.ToArray();
}
}
}
@@ -0,0 +1,28 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Operation for subscribing to funding info updates
/// </summary>
public interface ISubscribeFundingInfoSocket : ISharedSubscription
{
/// <summary>
/// Funding info subscription options
/// </summary>
SubscribeFundingInfoOptions SubscribeFundingInfoOptions { get; }
/// <summary>
/// Subscribe to funding info updates
/// </summary>
/// <param name="request">Request info</param>
/// <param name="handler">Update handler</param>
/// <param name="ct">Cancellation token, can be used to stop the updates</param>
/// <returns></returns>
Task<WebSocketResult<UpdateSubscription>> SubscribeToFundingInfoUpdatesAsync(SubscribeFundingInfoRequest request, Action<DataEvent<SharedFundingInfo>> handler, CancellationToken ct = default);
}
}
@@ -0,0 +1,29 @@
using CryptoExchange.Net.Objects;
using System;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for subscribing to funding info updates
/// </summary>
public class SubscribeFundingInfoOptions : CapabilityOptions<SubscribeFundingInfoRequest, ISubscribeFundingInfoSocket>
{
/// <inheritdoc />
public override string Description => "Subscribe to funding info updates for a symbol";
private static readonly RequestParameterDescription[] _defaultParameterRules = new[]
{
RequestParameterRule<SubscribeFundingInfoRequest>.Optional(x => x.Symbol, "The symbol to subscribe to", new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT")),
RequestParameterRule<SubscribeFundingInfoRequest>.Optional(x => x.Symbols, "The symbols to subscribe to", new[] { new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT") }),
};
/// <summary>
/// ctor
/// </summary>
public SubscribeFundingInfoOptions(string exchange, bool needsAuthentication)
: base(exchange, needsAuthentication, nameof(ISubscribeFundingInfoSocket.SubscribeToFundingInfoUpdatesAsync), _defaultParameterRules, SharedTradingModeSets.Futures)
{
}
}
}
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Request to subscribe to funding info updates for a symbol
/// </summary>
public record SubscribeFundingInfoRequest : SharedSymbolRequest
{
/// <summary>
/// ctor
/// </summary>
/// <param name="symbol">The symbol to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeFundingInfoRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null)
: base(symbol, exchangeParameters)
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="symbols">The symbols to subscribe to</param>
/// <param name="exchangeParameters">Exchange specific parameters</param>
public SubscribeFundingInfoRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null)
: base(symbols, exchangeParameters)
{
}
}
}
@@ -1,13 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Shared WebSocket API subscription capabitlity
/// Shared WebSocket API subscription capability
/// </summary>
public interface ISharedSubscription : ISharedSocket
{
/// <summary>
/// Unsubscribes all subscriptions created by the underlying socket API
/// client and closes its subscription connections.
/// </summary>
Task UnsubscribeAllAsync();
}
}
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Order book subscription options
/// </summary>
SubscribeOrderBookOptions SubscribeOrderBookOptions { get; }
SubscribeIncrementalOrderBookOptions SubscribeIncrementalOrderBookOptions { get; }
/// <summary>
/// Subscribe to incremental order book updates for a symbol
@@ -23,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis
/// <param name="handler">Update handler</param>
/// <param name="ct">Cancellation token, can be used to stop the updates</param>
/// <returns></returns>
Task<WebSocketResult<UpdateSubscription>> SubscribeToOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action<DataEvent<SharedOrderBook>> handler, CancellationToken ct = default);
Task<WebSocketResult<UpdateSubscription>> SubscribeToIncrementalOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action<DataEvent<SharedIncrementalOrderBook>> handler, CancellationToken ct = default);
}
}
@@ -33,7 +33,7 @@ namespace CryptoExchange.Net.SharedApis
/// ctor
/// </summary>
public SubscribeIncrementalOrderBookOptions(string exchange, bool needsAuthentication, int[] limits, SharedOrderBookSubscriptionType updateType)
: base(exchange, needsAuthentication, nameof(ISubscribeIncrementalOrderBookSocket.SubscribeToOrderBookUpdatesAsync), _defaultParameterRules)
: base(exchange, needsAuthentication, nameof(ISubscribeIncrementalOrderBookSocket.SubscribeToIncrementalOrderBookUpdatesAsync), _defaultParameterRules)
{
SupportedLimits = limits;
UpdateType = updateType;
@@ -424,6 +424,9 @@ namespace CryptoExchange.Net.Sockets.Default
{
try
{
if ((ApiClient.ApiOptions.DelayAfterConnect ?? ApiClient.ClientOptions.DelayAfterConnect) != TimeSpan.Zero)
await Task.Delay(ApiClient.ApiOptions.DelayAfterConnect ?? ApiClient.ClientOptions.DelayAfterConnect).ConfigureAwait(false);
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
if (!reconnectSuccessful.Success)
{
@@ -616,7 +619,17 @@ namespace CryptoExchange.Net.Sockets.Default
/// Connect the websocket
/// </summary>
/// <returns></returns>
public async Task<CallResult> ConnectAsync(CancellationToken ct) => await _socket.ConnectAsync(ct).ConfigureAwait(false);
public async Task<CallResult> ConnectAsync(CancellationToken ct)
{
var result = await _socket.ConnectAsync(ct).ConfigureAwait(false);
if (!result.Success)
return result;
if ((ApiClient.ApiOptions.DelayAfterConnect ?? ApiClient.ClientOptions.DelayAfterConnect) != TimeSpan.Zero)
await Task.Delay(ApiClient.ApiOptions.DelayAfterConnect ?? ApiClient.ClientOptions.DelayAfterConnect).ConfigureAwait(false);
return result;
}
/// <summary>
/// Retrieve the underlying socket
@@ -689,7 +702,7 @@ namespace CryptoExchange.Net.Sockets.Default
if (subscription.CancellationTokenRegistration.HasValue)
subscription.CancellationTokenRegistration.Value.Dispose();
bool anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageRouter.Routes.All(l => subscription.MessageRouter.ContainsCheck(l)));
bool anyDuplicateSubscription = SubscriptionIsCovered(subscription);
bool shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
if (!anyDuplicateSubscription)
@@ -1056,6 +1069,32 @@ namespace CryptoExchange.Net.Sockets.Default
return CallResult.Ok();
}
/// <summary>
/// Whether there is currently a subscription covering all the routes the subscription lists
/// </summary>
protected bool SubscriptionIsCovered(Subscription subscription)
=> _listeners.OfType<Subscription>().Any(
x => x != subscription
&& x.Active
&& subscription.MessageRouter.Routes.All(l => x.MessageRouter.ContainsCheck(l)));
/// <summary>
/// Whether there is a subscription earlier in the list covering all the routes the subscription lists
/// </summary>
protected bool IsCoveredByEarlierSubscription(Subscription subscription)
{
foreach (var other in _listeners.OfType<Subscription>())
{
if (other == subscription)
return false;
if (other.Active && subscription.MessageRouter.Routes.All(x => other.MessageRouter.ContainsCheck(x)))
return true;
}
return false;
}
/// <summary>
/// Try to subscribe a new subscription by sending the subscribe query and wait for the result as needed
/// </summary>
@@ -1090,11 +1129,11 @@ namespace CryptoExchange.Net.Sockets.Default
return WebSocketResult.Ok(ApiClient.Exchange, SocketId, default, 0, ConnectionUriString);
}
var subCompleteHandler = () =>
var subCompleteHandler = (bool success) =>
{
subscription.Status = subQuery.Success ? SubscriptionStatus.Subscribed : SubscriptionStatus.Pending;
subscription.Status = success ? SubscriptionStatus.Subscribed : SubscriptionStatus.Pending;
subscription.HandleSubQueryResponse(this, subQuery.Response);
if (newSubscription && subQuery.Success)
if (newSubscription && success)
{
subscription.TokenExpired += HandleTokenExpired;
if (subCancelToken != default)
@@ -1109,6 +1148,14 @@ namespace CryptoExchange.Net.Sockets.Default
};
subQuery.OnComplete = subCompleteHandler;
// For duplicate subscription don't send another subscribe request but just handle it internally
// Unsubscribe also doesn't unsubscribe when there is still a duplicate subscription running
if (IsCoveredByEarlierSubscription(subscription))
{
subQuery.Complete();
return WebSocketResult.Ok(ApiClient.Exchange, SocketId, default, subQuery.Id, ConnectionUriString);
}
var subQueryResult = await SendAndWaitQueryAsync(subQuery).ConfigureAwait(false);
if (!subQueryResult.Success)
{
@@ -1119,9 +1166,6 @@ namespace CryptoExchange.Net.Sockets.Default
return WebSocketResult.Fail(ApiClient.Exchange, subQueryResult.Error!);
}
if (!subQuery.ExpectsResponse)
subCompleteHandler();
return subQueryResult;
}
@@ -198,8 +198,13 @@ namespace CryptoExchange.Net.Sockets.HighPerf
{
var result = await _socket.ConnectAsync(ct).ConfigureAwait(false);
if (result.Success)
{
_processTask = ProcessAsync(_cts.Token);
if ((ApiClient.ApiOptions.DelayAfterConnect ?? ApiClient.ClientOptions.DelayAfterConnect) != TimeSpan.Zero)
await Task.Delay(ApiClient.ApiOptions.DelayAfterConnect ?? ApiClient.ClientOptions.DelayAfterConnect).ConfigureAwait(false);
}
return result;
}
+22 -6
View File
@@ -124,7 +124,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// On complete callback
/// </summary>
public Action? OnComplete { get; set; }
public Action<bool>? OnComplete { get; set; }
/// <inheritdoc />
public event Action? OnMessageRouterUpdated;
@@ -157,8 +157,7 @@ namespace CryptoExchange.Net.Sockets
}
else
{
Completed = true;
_event.Set();
Complete();
}
}
@@ -175,6 +174,11 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public abstract void Timeout();
/// <summary>
/// Mark request as successfully completed
/// </summary>
public abstract void Complete();
/// <summary>
/// Mark request as failed
/// </summary>
@@ -248,7 +252,7 @@ namespace CryptoExchange.Net.Sockets
{
Completed = true;
_event.Set();
OnComplete?.Invoke();
OnComplete?.Invoke(Success);
}
return handled;
@@ -267,7 +271,19 @@ namespace CryptoExchange.Net.Sockets
Completed = true;
_event.Set();
OnComplete?.Invoke();
OnComplete?.Invoke(Success);
}
/// <inheritdoc />
public override void Complete()
{
if (Completed)
return;
Result = CallResult<THandlerResponse>.Ok(default!);
Completed = true;
_event.Set();
OnComplete?.Invoke(Success);
}
/// <inheritdoc />
@@ -280,7 +296,7 @@ namespace CryptoExchange.Net.Sockets
Completed = true;
_event.Set();
OnComplete?.Invoke();
OnComplete?.Invoke(Success);
}
}
}
+29 -30
View File
@@ -5,37 +5,36 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="13.5.0" />
<PackageReference Include="Bitfinex.Net" Version="11.4.0" />
<PackageReference Include="BitMart.Net" Version="4.4.0" />
<PackageReference Include="BloFin.Net" Version="3.4.0" />
<PackageReference Include="Bybit.Net" Version="7.5.0" />
<PackageReference Include="CoinEx.Net" Version="11.4.0" />
<PackageReference Include="CoinW.Net" Version="3.4.0" />
<PackageReference Include="CryptoCom.Net" Version="4.4.0" />
<PackageReference Include="DeepCoin.Net" Version="4.4.0" />
<PackageReference Include="GateIo.Net" Version="4.6.0" />
<PackageReference Include="HyperLiquid.Net" Version="5.7.0" />
<PackageReference Include="JK.BingX.Net" Version="4.5.0" />
<PackageReference Include="JK.Bitget.Net" Version="4.5.0" />
<PackageReference Include="JK.Mexc.Net" Version="6.6.0" />
<PackageReference Include="JK.OKX.Net" Version="5.4.0" />
<PackageReference Include="Jkorf.Aster.Net" Version="4.4.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="4.4.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="4.5.0" />
<PackageReference Include="JKorf.HTX.Net" Version="9.5.0" />
<PackageReference Include="JKorf.Lighter.Net" Version="1.5.0" />
<PackageReference Include="JKorf.Upbit.Net" Version="3.4.0" />
<PackageReference Include="KrakenExchange.Net" Version="8.4.0" />
<PackageReference Include="Kucoin.Net" Version="9.5.0" />
<PackageReference Include="LBank.Net" Version="1.1.0" />
<PackageReference Include="Pionex.Net" Version="1.2.0" />
<PackageReference Include="Binance.Net" Version="13.6.0" />
<PackageReference Include="Bitfinex.Net" Version="11.6.0" />
<PackageReference Include="BitMart.Net" Version="4.5.0" />
<PackageReference Include="BloFin.Net" Version="3.5.0" />
<PackageReference Include="Bybit.Net" Version="7.6.0" />
<PackageReference Include="CoinEx.Net" Version="11.5.0" />
<PackageReference Include="CoinW.Net" Version="3.5.0" />
<PackageReference Include="CryptoCom.Net" Version="4.5.0" />
<PackageReference Include="DeepCoin.Net" Version="4.5.0" />
<PackageReference Include="GateIo.Net" Version="4.7.0" />
<PackageReference Include="HyperLiquid.Net" Version="5.8.0" />
<PackageReference Include="JK.BingX.Net" Version="4.6.0" />
<PackageReference Include="JK.Bitget.Net" Version="4.6.0" />
<PackageReference Include="JK.Mexc.Net" Version="6.7.0" />
<PackageReference Include="JK.OKX.Net" Version="5.5.0" />
<PackageReference Include="Jkorf.Aster.Net" Version="4.5.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="4.6.0" />
<PackageReference Include="JKorf.HTX.Net" Version="9.6.0" />
<PackageReference Include="JKorf.Lighter.Net" Version="1.6.0" />
<PackageReference Include="JKorf.Upbit.Net" Version="3.5.0" />
<PackageReference Include="KrakenExchange.Net" Version="8.5.0" />
<PackageReference Include="Kucoin.Net" Version="9.6.0" />
<PackageReference Include="LBank.Net" Version="1.2.0" />
<PackageReference Include="Pionex.Net" Version="1.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Tapbit.Net" Version="1.0.0" />
<PackageReference Include="Toobit.Net" Version="4.4.0" />
<PackageReference Include="Weex.Net" Version="2.4.0" />
<PackageReference Include="WhiteBit.Net" Version="4.4.0" />
<PackageReference Include="XT.Net" Version="4.5.0" />
<PackageReference Include="Tapbit.Net" Version="1.1.0" />
<PackageReference Include="Toobit.Net" Version="4.5.0" />
<PackageReference Include="Weex.Net" Version="2.5.0" />
<PackageReference Include="WhiteBit.Net" Version="4.5.0" />
<PackageReference Include="XT.Net" Version="4.6.0" />
</ItemGroup>
</Project>
+1 -6
View File
@@ -5,7 +5,6 @@
@inject IBitfinexRestClient bitfinexClient
@inject IBitgetRestClient bitgetClient
@inject IBitMartRestClient bitmartClient
@inject IBitMEXRestClient bitmexClient
@inject IBloFinRestClient bloFinClient
@inject IBybitRestClient bybitClient
@inject ICoinbaseRestClient coinbaseClient
@@ -47,7 +46,6 @@
var bitfinexTask = bitfinexClient.ExchangeApi.ExchangeData.GetTickerAsync("tBTCUSD");
var bitgetTask = bitgetClient.SpotApiV2.ExchangeData.GetTickersAsync("BTCUSDT");
var bitmartTask = bitmartClient.SpotApi.ExchangeData.GetTickerAsync("BTC_USDT");
var bitmexTask = bitmexClient.ExchangeApi.ExchangeData.GetSymbolsAsync("XBT_USDT");
var bloFinTask = bloFinClient.FuturesApi.ExchangeData.GetTickersAsync("BTC-USDT");
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");
@@ -71,7 +69,7 @@
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,
await Task.WhenAll(asterTask, binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bloFinTask, bybitTask, coinexTask, coinWTask, deepCoinTask, gateioTask,
htxTask, krakenTask, kucoinTask, lBankTask, mexcTask, okxTask, pionexTask, toobitTask, upbitTask, weexTask, whitebitTask, xtTask);
if (asterTask.Result.Success)
@@ -92,9 +90,6 @@
if (bitmartTask.Result.Success)
_prices.Add("BitMart", bitmartTask.Result.Data.LastPrice);
if (bitmexTask.Result.Success)
_prices.Add("BitMEX", bitmexTask.Result.Data.First().LastPrice);
if (bloFinTask.Result.Success)
_prices.Add("BloFin", bloFinTask.Result.Data.First().LastPrice);
@@ -5,7 +5,6 @@
@inject IBitfinexSocketClient bitfinexSocketClient
@inject IBitgetSocketClient bitgetSocketClient
@inject IBitMartSocketClient bitmartSocketClient
@inject IBitMEXSocketClient bitmexSocketClient
@inject IBloFinSocketClient bloFinSocketClient
@inject IBybitSocketClient bybitSocketClient
@inject ICoinbaseSocketClient coinbaseSocketClient
@@ -56,7 +55,6 @@
bitfinexSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
bitgetSocketClient.SpotApiV2.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.First().LastPrice)),
bitmartSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("BitMart", data.Data.LastPrice)),
bitmexSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH_XBT", data => UpdateData("BitMEX", data.Data.LastPrice ?? 0)),
// BloFin doesn't support the ETH/BTC pair
//bloFinSocketClient.FuturesApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BloFin", data.Data.LastPrice)),
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
@@ -7,7 +7,6 @@
@using Bitfinex.Net.Interfaces
@using Bitget.Net.Interfaces;
@using BitMart.Net.Interfaces;
@using BitMEX.Net.Interfaces;
@using BloFin.Net.Interfaces;
@using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@@ -40,7 +39,6 @@
@inject IBitfinexOrderBookFactory bitfinexFactory
@inject IBitgetOrderBookFactory bitgetFactory
@inject IBitMartOrderBookFactory bitmartFactory
@inject IBitMEXOrderBookFactory bitmexFactory
@inject IBloFinOrderBookFactory bloFinFactory
@inject IBybitOrderBookFactory bybitFactory
@inject ICoinbaseOrderBookFactory coinbaseFactory
@@ -103,7 +101,6 @@
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
{ "BitMart", bitmartFactory.CreateSpot("ETH_BTC", null) },
{ "BitMEX", bitmexFactory.Create("ETH_XBT") },
{ "BloFin", bloFinFactory.CreateFutures("ETH-USDT") },
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
+2 -2
View File
@@ -1,7 +1,7 @@
@page "/SpotClient"
@using CryptoExchange.Net.SharedApis
@using System.Diagnostics
@inject IEnumerable<ISpotTickerRestClient> restClients
@inject IEnumerable<IGetTicker> restClients
<h3>ETH-BTC prices:</h3>
@foreach(var price in _prices.OrderBy(p => p.Key))
@@ -15,7 +15,7 @@
protected override async Task OnInitializedAsync()
{
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "BTC");
var tasks = restClients.Select(x => x.GetSpotTickerAsync(new GetTickerRequest(symbol)));
var tasks = restClients.Where(x => x.SupportedTradingModes.Contains(TradingMode.Spot)).Select(x => x.GetTickerAsync(new GetTickerRequest(symbol)));
await foreach (var ticker in ParallelEnumerateAsync(tasks))
{
if (ticker.Success)
@@ -6,7 +6,6 @@
@using BingX.Net.Interfaces
@using Bitfinex.Net.Interfaces
@using Bitget.Net.Interfaces;
@using BitMEX.Net.Interfaces;
@using BitMart.Net.Interfaces;
@using BloFin.Net.Interfaces
@using Bybit.Net.Interfaces
@@ -40,7 +39,6 @@
@inject IBitfinexTrackerFactory bitfinexFactory
@inject IBitgetTrackerFactory bitgetFactory
@inject IBitMartTrackerFactory bitmartFactory
@inject IBitMEXTrackerFactory bitmexFactory
@inject IBloFinTrackerFactory bloFinFactory
@inject IBybitTrackerFactory bybitFactory
@inject ICoinbaseTrackerFactory coinbaseFactory
@@ -96,7 +94,6 @@
{ bitfinexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ bitgetFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ bitmartFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ bitmexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ bloFinFactory.CreateTradeTracker(futuresSymbol, period: TimeSpan.FromMinutes(5)) },
{ bybitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ coinbaseFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
-1
View File
@@ -39,7 +39,6 @@ namespace BlazorClient
services.AddBitfinex();
services.AddBitget();
services.AddBitMart();
services.AddBitMEX();
services.AddBloFin();
services.AddBybit();
services.AddCoinbase();
-1
View File
@@ -14,7 +14,6 @@
@using Bitfinex.Net.Interfaces.Clients;
@using Bitget.Net.Interfaces.Clients;
@using BitMart.Net.Interfaces.Clients;
@using BitMEX.Net.Interfaces.Clients;
@using BloFin.Net.Interfaces.Clients;
@using Bybit.Net.Interfaces.Clients;
@using Coinbase.Net.Interfaces.Clients;
+2 -2
View File
@@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="12.13.0" />
<PackageReference Include="Bybit.Net" Version="6.13.0" />
<PackageReference Include="Binance.Net" Version="13.6.0" />
<PackageReference Include="Bybit.Net" Version="7.6.0" />
</ItemGroup>
</Project>
+26 -34
View File
@@ -25,18 +25,22 @@ var exchanges = new Dictionary<string, ExchangeClients>(StringComparer.OrdinalIg
{
["binance"] = new ExchangeClients(
"Binance",
binanceRest.SpotApi.SharedClient,
binanceRest.SpotApi.SharedClient,
binanceRest.SpotApi.SharedClient,
binanceRest.SpotApi.SharedClient,
binanceSocket.SpotApi.SharedClient),
binanceRest.SpotApi.SharedApi,
binanceRest.SpotApi.SharedApi,
binanceRest.SpotApi.SharedApi,
binanceRest.SpotApi.SharedApi,
binanceRest.SpotApi.SharedApi,
binanceRest.SpotApi.SharedApi,
binanceSocket.SpotApi.SharedApi),
["bybit"] = new ExchangeClients(
"Bybit",
bybitRest.V5Api.SharedClient,
bybitRest.V5Api.SharedClient,
bybitRest.V5Api.SharedClient,
bybitRest.V5Api.SharedClient,
bybitSocket.V5SpotApi.SharedClient)
bybitRest.V5Api.SharedApi,
bybitRest.V5Api.SharedApi,
bybitRest.V5Api.SharedApi,
bybitRest.V5Api.SharedApi,
bybitRest.V5Api.SharedApi,
bybitRest.V5Api.SharedApi,
bybitSocket.V5SpotApi.SharedApi)
};
PrintHelp();
@@ -98,7 +102,7 @@ async Task PrintPricesAsync(string[] args)
var symbol = GetSymbol(args, 1);
var tasks = exchanges.Values.Select(async exchange =>
{
var result = await exchange.TickerRest.GetSpotTickerAsync(new GetTickerRequest(symbol));
var result = await exchange.TickerRest.GetTickerAsync(new GetTickerRequest(symbol));
if (!result.Success)
{
Console.WriteLine($"{exchange.Name,-8} error: {result.Error}");
@@ -171,7 +175,7 @@ async Task PrintOpenOrdersAsync(string[] args)
var exchange = GetExchange(args, 1);
var symbol = GetSymbol(args, 2);
var result = await exchange.OrderRest.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol));
var result = await exchange.GetOpenOrdersRest.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol));
if (!result.Success)
{
Console.WriteLine($"Open orders request failed: {result.Error}");
@@ -203,23 +207,9 @@ async Task PlaceLimitOrderAsync(string[] args)
SharedQuantity.Base(quantity),
price,
SharedTimeInForce.GoodTillCanceled,
exchange.OrderRest.GenerateClientOrderId());
exchange.PlaceOrderRest.GenerateClientOrderId());
var validationError = exchange.OrderRest.PlaceSpotOrderOptions.ValidateRequest(
exchange.Name,
request,
TradingMode.Spot,
exchange.OrderRest.SupportedTradingModes,
exchange.OrderRest.SpotSupportedOrderTypes,
exchange.OrderRest.SpotSupportedTimeInForce,
exchange.OrderRest.SpotSupportedOrderQuantity);
if (validationError != null)
{
Console.WriteLine($"Order request is not valid for {exchange.Name}: {validationError}");
return;
}
var result = await exchange.OrderRest.PlaceSpotOrderAsync(request);
var result = await exchange.PlaceOrderRest.PlaceSpotOrderAsync(request);
Console.WriteLine(result.Success
? $"Order placed. Id: {result.Data.Id}"
: $"Order failed: {result.Error}");
@@ -233,7 +223,7 @@ async Task CancelOrderAsync(string[] args)
if (string.IsNullOrWhiteSpace(orderId))
throw new ArgumentException("Missing order id. Example: cancel-order binance BTC USDT 123456");
var result = await exchange.OrderRest.CancelSpotOrderAsync(new CancelOrderRequest(symbol, orderId));
var result = await exchange.CancelOrderRest.CancelSpotOrderAsync(new CancelOrderRequest(symbol, orderId));
Console.WriteLine(result.Success
? $"Order canceled. Id: {result.Data.Id}"
: $"Cancel failed: {result.Error}");
@@ -296,8 +286,10 @@ static void PrintHelp()
internal record ExchangeClients(
string Name,
ISpotTickerRestClient TickerRest,
IOrderBookRestClient OrderBookRest,
IBalanceRestClient BalanceRest,
ISpotOrderRestClient OrderRest,
ITickerSocketClient TickerSocket);
IGetTickerRest TickerRest,
IGetOrderBookRest OrderBookRest,
IGetBalancesRest BalanceRest,
IPlaceSpotOrderRest PlaceOrderRest,
IGetOpenSpotOrdersRest GetOpenOrdersRest,
ICancelSpotOrderRest CancelOrderRest,
ISubscribeTickerSocket TickerSocket);
+9 -9
View File
@@ -5,13 +5,13 @@ using CryptoExchange.Net.SharedApis;
using OKX.Net.Clients;
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
var binanceSpotRestClient = new BinanceRestClient().SpotApi.SharedClient;
var okxSpotRestClient = new OKXRestClient().UnifiedApi.SharedClient;
var bitmartSpotRestClient = new BitMartRestClient().SpotApi.SharedClient;
var binanceSpotRestClient = new BinanceRestClient().SpotApi.SharedApi;
var okxSpotRestClient = new OKXRestClient().UnifiedApi.SharedApi;
var bitmartSpotRestClient = new BitMartRestClient().SpotApi.SharedApi;
var binanceSpotSocketClient = new BinanceSocketClient().SpotApi.SharedClient;
var okxSpotSocketClient = new OKXSocketClient().UnifiedApi.SharedClient;
var bitmartSpotSocketClient = new BitMartSocketClient().SpotApi.SharedClient;
var binanceSpotSocketClient = new BinanceSocketClient().SpotApi.SharedApi;
var okxSpotSocketClient = new OKXSocketClient().UnifiedApi.SharedApi;
var bitmartSpotSocketClient = new BitMartSocketClient().SpotApi.SharedApi;
await GetLastTradePriceAsync(binanceSpotRestClient, symbol);
await GetLastTradePriceAsync(okxSpotRestClient, symbol);
@@ -31,9 +31,9 @@ Console.ReadLine();
foreach (var subscription in subscriptions)
await subscription.CloseAsync();
async Task GetLastTradePriceAsync(ISpotTickerRestClient client, SharedSymbol symbol)
async Task GetLastTradePriceAsync(IGetTickerRest client, SharedSymbol symbol)
{
var result = await client.GetSpotTickerAsync(new GetTickerRequest(symbol));
var result = await client.GetTickerAsync(new GetTickerRequest(symbol));
if (!result.Success)
{
Console.WriteLine($"Failed to get ticker: {result.Error}");
@@ -43,7 +43,7 @@ async Task GetLastTradePriceAsync(ISpotTickerRestClient client, SharedSymbol sym
Console.WriteLine($"{client.Exchange} {result.Data.Symbol}: {result.Data.LastPrice}");
}
async Task SubscribeTickerUpdatesAsync(ITickerSocketClient client, SharedSymbol symbol, ICollection<UpdateSubscription> subscriptions)
async Task SubscribeTickerUpdatesAsync(ISubscribeTickerSocket client, SharedSymbol symbol, ICollection<UpdateSubscription> subscriptions)
{
var result = await client.SubscribeToTickerUpdatesAsync(new SubscribeTickerRequest(symbol), update =>
{
+3 -3
View File
@@ -8,9 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="12.13.0" />
<PackageReference Include="BitMart.Net" Version="3.11.2" />
<PackageReference Include="JK.OKX.Net" Version="4.14.0" />
<PackageReference Include="Binance.Net" Version="13.6.0" />
<PackageReference Include="BitMart.Net" Version="4.5.0" />
<PackageReference Include="JK.OKX.Net" Version="5.5.0" />
</ItemGroup>
</Project>
+14
View File
@@ -127,6 +127,20 @@ Various:
* PlatformInfo now required support environment names in the constructor
## Release notes
* Version 13.2.0 - 13 Oct 2026
* Added Shared API ISubscribeFundingInfoSocket
* Added DelayAfterConnect to SocketApiOptions
* Added processor parameter for some methods in SocketApiClient to allow specific overriding behavior
* Updated WebSocket duplicate subscription handling
* Updated Shared APIs ISubscribeIncrementalOrderBook
* Fixed incorrect status keeping of WebSocket subscriptions which don't expect a response
* Fixed DelayAfterConnect not being respected on reconnection
* Version 13.1.0 - 28 Sep 2026
* Added UnsubscribeAllAsync method to Shared APIs ISharedSubscription and ISharedApiClientBase
* Added WaitAllAsync extension method for waiting on an IAsyncEnumerable to finish
* Added SubscribeAllAsync extension methods for Shared API resolved capabilities
* Version 13.0.0 - 23 Sep 2026
* Shared APIs
* Added Shared API V2 with fine-grained capability interfaces for individual REST requests, WebSocket requests and subscriptions
+2 -2
View File
@@ -1,8 +1,8 @@
# CryptoExchange.Net — full AI context
Library: CryptoExchange.Net
Library version: 13.0.0
CryptoExchange.Net package version: 13.0.0
Library version: 13.2.0
CryptoExchange.Net package version: 13.2.0
Language: C#/.NET
Targets: netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0
+1 -1
View File
@@ -2,7 +2,7 @@
> Base C#/.NET library used by 28+ cryptocurrency exchange clients. It standardizes REST, WebSocket, authentication, rate limiting, results, and exchange-agnostic Shared APIs.
CryptoExchange.Net is not an exchange client by itself. Install exchange-specific packages such as Binance.Net, Bybit.Net, JK.OKX.Net, Kraken.Net, or Coinbase.Net, or install `CryptoClients.Net` for the bundle. Current release: 13.0.0. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, and net10.0; Native AOT is supported.
CryptoExchange.Net is not an exchange client by itself. Install exchange-specific packages such as Binance.Net, Bybit.Net, JK.OKX.Net, Kraken.Net, or Coinbase.Net, or install `CryptoClients.Net` for the bundle. Current release: 13.2.0. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, and net10.0; Native AOT is supported.
## Shared API V2