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

Compare commits

...

8 Commits

23 changed files with 407 additions and 246 deletions
@@ -108,5 +108,33 @@ namespace CryptoExchange.Net.UnitTests
Assert.AreEqual(1.06666667m, resultBids2.Data);
Assert.AreEqual(1.23333333m, resultAsks2.Data);
}
[TestCase]
public void CalculateTradableAmount()
{
var orderbook = new TestableSymbolOrderBook();
orderbook.SetData(new List<ISymbolOrderBookEntry>
{
new BookEntry{ Price = 1, Quantity = 1 },
new BookEntry{ Price = 1.1m, Quantity = 1 },
},
new List<ISymbolOrderBookEntry>()
{
new BookEntry{ Price = 1.2m, Quantity = 1 },
new BookEntry{ Price = 1.3m, Quantity = 1 },
});
var resultBids = orderbook.CalculateTradableAmount(2, OrderBookEntryType.Bid);
var resultAsks = orderbook.CalculateTradableAmount(2, OrderBookEntryType.Ask);
var resultBids2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Bid);
var resultAsks2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Ask);
Assert.True(resultBids.Success);
Assert.True(resultAsks.Success);
Assert.AreEqual(1.9m, resultBids.Data);
Assert.AreEqual(1.61538462m, resultAsks.Data);
Assert.AreEqual(1.4m, resultBids2.Data);
Assert.AreEqual(1.23076923m, resultAsks2.Data);
}
}
}
@@ -37,8 +37,8 @@ namespace CryptoExchange.Net.UnitTests
public CallResult<T> Deserialize<T>(string data) => Deserialize<T>(data, null, null);
public override TimeSpan GetTimeOffset() => throw new NotImplementedException();
public override TimeSyncInfo GetTimeSyncInfo() => throw new NotImplementedException();
public override TimeSpan? GetTimeOffset() => null;
public override TimeSyncInfo GetTimeSyncInfo() => null;
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
}
@@ -142,7 +142,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
ParameterPositions[method] = position;
}
public override TimeSpan GetTimeOffset()
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
@@ -178,7 +178,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
}
public override TimeSpan GetTimeOffset()
public override TimeSpan? GetTimeOffset()
{
throw new NotImplementedException();
}
@@ -21,20 +21,6 @@ namespace CryptoExchange.Net.Authentication
/// </summary>
public SecureString? Secret { get; }
/// <summary>
/// The private key to authenticate requests
/// </summary>
public PrivateKey? PrivateKey { get; }
/// <summary>
/// Create Api credentials providing a private key for authentication
/// </summary>
/// <param name="privateKey">The private key used for signing</param>
public ApiCredentials(PrivateKey privateKey)
{
PrivateKey = privateKey;
}
/// <summary>
/// Create Api credentials providing an api key and secret for authentication
/// </summary>
@@ -69,11 +55,8 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns>
public virtual ApiCredentials Copy()
{
if (PrivateKey == null)
// Use .GetString() to create a copy of the SecureString
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
else
return new ApiCredentials(PrivateKey!.Copy());
// Use .GetString() to create a copy of the SecureString
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
}
/// <summary>
@@ -123,7 +106,6 @@ namespace CryptoExchange.Net.Authentication
{
Key?.Dispose();
Secret?.Dispose();
PrivateKey?.Dispose();
}
}
}
@@ -223,7 +223,7 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns>
protected static DateTime GetTimestamp(RestApiClient apiClient)
{
return DateTime.UtcNow.Add(apiClient?.GetTimeOffset() ?? TimeSpan.Zero)!;
return DateTime.UtcNow.Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
}
/// <summary>
@@ -1,110 +0,0 @@
using System;
using System.Security;
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Private key info
/// </summary>
public class PrivateKey : IDisposable
{
/// <summary>
/// The private key
/// </summary>
public SecureString Key { get; }
/// <summary>
/// The private key's pass phrase
/// </summary>
public SecureString? Passphrase { get; }
/// <summary>
/// Indicates if the private key is encrypted or not
/// </summary>
public bool IsEncrypted { get; }
/// <summary>
/// Create a private key providing an encrypted key information
/// </summary>
/// <param name="key">The private key used for signing</param>
/// <param name="passphrase">The private key's passphrase</param>
public PrivateKey(SecureString key, SecureString passphrase)
{
Key = key;
Passphrase = passphrase;
IsEncrypted = true;
}
/// <summary>
/// Create a private key providing an encrypted key information
/// </summary>
/// <param name="key">The private key used for signing</param>
/// <param name="passphrase">The private key's passphrase</param>
public PrivateKey(string key, string passphrase)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(passphrase))
throw new ArgumentException("Key and passphrase can't be null/empty");
var secureKey = new SecureString();
foreach (var c in key)
secureKey.AppendChar(c);
secureKey.MakeReadOnly();
Key = secureKey;
var securePassphrase = new SecureString();
foreach (var c in passphrase)
securePassphrase.AppendChar(c);
securePassphrase.MakeReadOnly();
Passphrase = securePassphrase;
IsEncrypted = true;
}
/// <summary>
/// Create a private key providing an unencrypted key information
/// </summary>
/// <param name="key">The private key used for signing</param>
public PrivateKey(SecureString key)
{
Key = key;
IsEncrypted = false;
}
/// <summary>
/// Create a private key providing an encrypted key information
/// </summary>
/// <param name="key">The private key used for signing</param>
public PrivateKey(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("Key can't be null/empty");
Key = key.ToSecureString();
IsEncrypted = false;
}
/// <summary>
/// Copy the private key
/// </summary>
/// <returns></returns>
public PrivateKey Copy()
{
if (Passphrase == null)
return new PrivateKey(Key.GetString());
else
return new PrivateKey(Key.GetString(), Passphrase.GetString());
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Key?.Dispose();
Passphrase?.Dispose();
}
}
}
+5 -4
View File
@@ -6,6 +6,7 @@ using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
@@ -17,7 +18,7 @@ namespace CryptoExchange.Net
/// <summary>
/// Base API for all API clients
/// </summary>
public abstract class BaseApiClient: IDisposable
public abstract class BaseApiClient : IDisposable, IBaseApiClient
{
private ApiCredentials? _apiCredentials;
private AuthenticationProvider? _authenticationProvider;
@@ -38,7 +39,7 @@ namespace CryptoExchange.Net
/// </summary>
public AuthenticationProvider? AuthenticationProvider
{
get
get
{
if (!_created && !_disposing && _apiCredentials != null)
{
@@ -98,7 +99,7 @@ namespace CryptoExchange.Net
/// <summary>
/// Lock for id generating
/// </summary>
protected static object idLock = new ();
protected static object idLock = new();
/// <summary>
/// A default serializer
@@ -131,7 +132,7 @@ namespace CryptoExchange.Net
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
/// <inheritdoc />
public void SetApiCredentials(ApiCredentials credentials)
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
_apiCredentials = credentials?.Copy();
_created = false;
+1 -1
View File
@@ -53,7 +53,7 @@ namespace CryptoExchange.Net
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
public void SetApiCredentials(ApiCredentials credentials)
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
{
foreach (var apiClient in ApiClients)
apiClient.SetApiCredentials(credentials);
+73 -48
View File
@@ -20,35 +20,24 @@ namespace CryptoExchange.Net
/// <summary>
/// Base rest API client for interacting with a REST API
/// </summary>
public abstract class RestApiClient: BaseApiClient
public abstract class RestApiClient : BaseApiClient, IRestApiClient
{
/// <summary>
/// The factory for creating requests. Used for unit testing
/// </summary>
/// <inheritdoc />
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
/// <inheritdoc />
public abstract TimeSyncInfo? GetTimeSyncInfo();
/// <inheritdoc />
public abstract TimeSpan? GetTimeOffset();
/// <inheritdoc />
public int TotalRequestsMade { get; set; }
/// <summary>
/// Request headers to be sent with each request
/// </summary>
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
/// <summary>
/// Get time sync info for an API client
/// </summary>
/// <returns></returns>
public abstract TimeSyncInfo GetTimeSyncInfo();
/// <summary>
/// Get time offset for an API client
/// </summary>
/// <returns></returns>
public abstract TimeSpan GetTimeOffset();
/// <summary>
/// Total amount of requests made with this API client
/// </summary>
public int TotalRequestsMade { get; set; }
/// <summary>
/// Options for this client
/// </summary>
@@ -70,7 +59,7 @@ namespace CryptoExchange.Net
/// <param name="log">Logger</param>
/// <param name="options">The base client options</param>
/// <param name="apiOptions">The Api client options</param>
public RestApiClient(Log log, ClientOptions options, RestApiClientOptions apiOptions): base(log, options, apiOptions)
public RestApiClient(Log log, ClientOptions options, RestApiClientOptions apiOptions) : base(log, options, apiOptions)
{
var rateLimiters = new List<IRateLimiter>();
foreach (var rateLimiter in apiOptions.RateLimiters)
@@ -110,12 +99,20 @@ namespace CryptoExchange.Net
Dictionary<string, string>? additionalHeaders = null,
bool ignoreRatelimit = false)
{
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult(request.Error!);
int currentTry = 0;
while (true)
{
currentTry++;
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult(request.Error!);
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
return result.AsDataless();
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
continue;
return result.AsDataless();
}
}
/// <summary>
@@ -149,11 +146,20 @@ namespace CryptoExchange.Net
bool ignoreRatelimit = false
) where T : class
{
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult<T>(request.Error!);
int currentTry = 0;
while (true)
{
currentTry++;
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
if (!request)
return new WebCallResult<T>(request.Error!);
return await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
var result = await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
continue;
return result;
}
}
/// <summary>
@@ -190,7 +196,8 @@ namespace CryptoExchange.Net
{
var syncTask = SyncTimeAsync();
var timeSyncInfo = GetTimeSyncInfo();
if (timeSyncInfo.TimeSyncState.LastSyncTime == default)
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
{
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
var syncTimeResult = await syncTask.ConfigureAwait(false);
@@ -206,7 +213,7 @@ namespace CryptoExchange.Net
{
foreach (var limiter in RateLimiters)
{
var limitResult = await limiter.LimitRequestAsync(_log, uri.AbsolutePath, method, signed, Options.ApiCredentials?.Key, Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
var limitResult = await limiter.LimitRequestAsync(_log, uri.AbsolutePath, method, signed, Options.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
if (!limitResult.Success)
return new CallResult<IRequest>(limitResult.Error!);
}
@@ -235,8 +242,6 @@ namespace CryptoExchange.Net
return new CallResult<IRequest>(request);
}
/// <summary>
/// Executes the request and returns the result deserialized into the type parameter class
/// </summary>
@@ -376,6 +381,16 @@ namespace CryptoExchange.Net
return Task.FromResult<ServerError?>(null);
}
/// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
/// Note that this is always called; even when the request might be successful
/// </summary>
/// <typeparam name="T">WebCallResult type parameter</typeparam>
/// <param name="callResult">The result of the call</param>
/// <param name="tries">The current try number</param>
/// <returns>True if call should retry, false if the call should return</returns>
protected virtual Task<bool> ShouldRetryRequestAsync<T>(WebCallResult<T> callResult, int tries) => Task.FromResult(false);
/// <summary>
/// Creates a request object
/// </summary>
@@ -418,17 +433,24 @@ namespace CryptoExchange.Net
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
if (AuthenticationProvider != null)
{
AuthenticationProvider.AuthenticateRequest(
this,
uri,
method,
parameters,
signed,
arraySerialization,
parameterPosition,
out uriParameters,
out bodyParameters,
out headers);
try
{
AuthenticationProvider.AuthenticateRequest(
this,
uri,
method,
parameters,
signed,
arraySerialization,
parameterPosition,
out uriParameters,
out bodyParameters,
out headers);
}
catch (Exception ex)
{
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
}
}
// Sanity check
@@ -514,11 +536,14 @@ namespace CryptoExchange.Net
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
/// </summary>
/// <returns>Server time</returns>
protected abstract Task<WebCallResult<DateTime>> GetServerTimestampAsync();
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
internal async Task<WebCallResult<bool>> SyncTimeAsync()
{
var timeSyncParams = GetTimeSyncInfo();
if (timeSyncParams == null)
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
{
if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval))
@@ -550,7 +575,7 @@ namespace CryptoExchange.Net
// Calculate time offset between local and server
var offset = result.Data - (localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2));
timeSyncParams.UpdateTimeOffset(offset);
timeSyncParams.TimeSyncState.Semaphore.Release();
timeSyncParams.TimeSyncState.Semaphore.Release();
}
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, true, null);
@@ -18,13 +18,10 @@ namespace CryptoExchange.Net
/// <summary>
/// Base socket API client for interaction with a websocket API
/// </summary>
public abstract class SocketApiClient : BaseApiClient
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
{
#region Fields
/// <summary>
/// The factory for creating sockets. Used for unit testing
/// </summary>
/// <inheritdoc/>
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
/// <summary>
@@ -117,7 +114,7 @@ namespace CryptoExchange.Net
/// <param name="log">log</param>
/// <param name="options">Client options</param>
/// <param name="apiOptions">The Api client options</param>
public SocketApiClient(Log log, ClientOptions options, SocketApiClientOptions apiOptions): base(log, options, apiOptions)
public SocketApiClient(Log log, ClientOptions options, SocketApiClientOptions apiOptions) : base(log, options, apiOptions)
{
ClientOptions = options;
}
@@ -265,7 +262,7 @@ namespace CryptoExchange.Net
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
{
CallResult<object>? callResult = null;
await socketConnection.SendAndWaitAsync(request, Options.SocketResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
await socketConnection.SendAndWaitAsync(request, Options.SocketResponseTimeout, subscription, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
if (callResult?.Success == true)
{
@@ -351,7 +348,7 @@ namespace CryptoExchange.Net
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
{
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
await socket.SendAndWaitAsync(request, Options.SocketResponseTimeout, data =>
await socket.SendAndWaitAsync(request, Options.SocketResponseTimeout, null, data =>
{
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
return false;
+4 -4
View File
@@ -6,16 +6,16 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>A base package for implementing cryptocurrency API's</Description>
<PackageVersion>5.3.1</PackageVersion>
<AssemblyVersion>5.3.1</AssemblyVersion>
<FileVersion>5.3.1</FileVersion>
<PackageVersion>5.4.1</PackageVersion>
<AssemblyVersion>5.4.1</AssemblyVersion>
<FileVersion>5.4.1</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>5.3.1 - Added default request parameter ordering before applying authentication, Fixed possible issue where a socket would reconnect when it should close if it was already in reconnecting</PackageReleaseNotes>
<PackageReleaseNotes>5.4.1 - Added CalculateTradableAmount to SymbolOrderBook, Improved socket reconnect robustness, Fixed api rate limiter not working correctly</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>9.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
@@ -0,0 +1,17 @@
using CryptoExchange.Net.Authentication;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Base api client
/// </summary>
public interface IBaseApiClient
{
/// <summary>
/// Set the API credentials for this API client
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="credentials"></param>
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
}
}
@@ -0,0 +1,33 @@
using CryptoExchange.Net.Objects;
using System;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Base rest API client
/// </summary>
public interface IRestApiClient : IBaseApiClient
{
/// <summary>
/// The factory for creating requests. Used for unit testing
/// </summary>
IRequestFactory RequestFactory { get; set; }
/// <summary>
/// Total amount of requests made with this API client
/// </summary>
int TotalRequestsMade { get; set; }
/// <summary>
/// Get time offset for an API client. Return null if time syncing shouldnt/cant be done
/// </summary>
/// <returns></returns>
TimeSpan? GetTimeOffset();
/// <summary>
/// Get time sync info for an API client. Return null if time syncing shouldnt/cant be done
/// </summary>
/// <returns></returns>
TimeSyncInfo? GetTimeSyncInfo();
}
}
@@ -18,11 +18,5 @@ namespace CryptoExchange.Net.Interfaces
/// The total amount of requests made with this client
/// </summary>
int TotalRequestsMade { get; }
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
void SetApiCredentials(ApiCredentials credentials);
}
}
@@ -0,0 +1,81 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using System;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Socket API client
/// </summary>
public interface ISocketApiClient: IBaseApiClient
{
/// <summary>
/// The current amount of socket connections on the API client
/// </summary>
int CurrentConnections { get; }
/// <summary>
/// The current amount of subscriptions over all connections
/// </summary>
int CurrentSubscriptions { get; }
/// <summary>
/// Incoming data kpbs
/// </summary>
double IncomingKbps { get; }
/// <summary>
/// Client options
/// </summary>
SocketApiClientOptions Options { get; }
/// <summary>
/// The factory for creating sockets. Used for unit testing
/// </summary>
IWebsocketFactory SocketFactory { get; set; }
/// <summary>
/// Get the url to reconnect to after losing a connection
/// </summary>
/// <param name="connection"></param>
/// <returns></returns>
Task<Uri?> GetReconnectUriAsync(SocketConnection connection);
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
string GetSubscriptionsState();
/// <summary>
/// Reconnect all connections
/// </summary>
/// <returns></returns>
Task ReconnectAsync();
/// <summary>
/// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
/// </summary>
/// <param name="request">The original request</param>
/// <returns></returns>
Task<CallResult<object>> RevitalizeRequestAsync(object request);
/// <summary>
/// Periodically sends data over a socket connection
/// </summary>
/// <param name="identifier">Identifier for the periodic send</param>
/// <param name="interval">How often</param>
/// <param name="objGetter">Method returning the object to send</param>
void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter);
/// <summary>
/// Unsubscribe all subscriptions
/// </summary>
/// <returns></returns>
Task UnsubscribeAllAsync();
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
/// <returns></returns>
Task<bool> UnsubscribeAsync(int subscriptionId);
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
/// <param name="subscription">The subscription to unsubscribe</param>
/// <returns></returns>
Task UnsubscribeAsync(UpdateSubscription subscription);
}
}
@@ -16,12 +16,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
ClientOptions ClientOptions { get; }
/// <summary>
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
/// </summary>
/// <param name="credentials">The credentials to set</param>
void SetApiCredentials(ApiCredentials credentials);
/// <summary>
/// Incoming kilobytes per second of data
/// </summary>
@@ -101,13 +101,22 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled
/// at that price since between this calculation and the order placement the book can have changed.
/// at that price since between this calculation and the order placement the book might have changed.
/// </summary>
/// <param name="quantity">The quantity in base asset to fill</param>
/// <param name="type">The type</param>
/// <returns>Average fill price</returns>
CallResult<decimal> CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type);
/// <summary>
/// Get the amount of base asset which can be traded with the quote quantity when placing a market order at at the current order book state.
/// This is no guarentee that an order of that quantity would actually be fill the quantity returned by this since between this calculation and the order placement the book might have changed.
/// </summary>
/// <param name="quoteQuantity">The quantity in quote asset looking to trade</param>
/// <param name="type">The type</param>
/// <returns>Amount of base asset tradable with the specified amount of quote asset</returns>
CallResult<decimal> CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type);
/// <summary>
/// String representation of the top x entries
/// </summary>
+8 -8
View File
@@ -298,14 +298,14 @@ namespace CryptoExchange.Net.Objects
if (baseOptions == null)
return;
AutoReconnect = baseOptions.AutoReconnect;
ReconnectInterval = baseOptions.ReconnectInterval;
MaxConcurrentResubscriptionsPerSocket = baseOptions.MaxConcurrentResubscriptionsPerSocket;
SocketResponseTimeout = baseOptions.SocketResponseTimeout;
SocketNoDataTimeout = baseOptions.SocketNoDataTimeout;
SocketSubscriptionsCombineTarget = baseOptions.SocketSubscriptionsCombineTarget;
MaxSocketConnections = baseOptions.MaxSocketConnections;
DelayAfterConnect = baseOptions.DelayAfterConnect;
AutoReconnect = newValues?.AutoReconnect ?? baseOptions.AutoReconnect;
ReconnectInterval = newValues?.ReconnectInterval ?? baseOptions.ReconnectInterval;
MaxConcurrentResubscriptionsPerSocket = newValues?.MaxConcurrentResubscriptionsPerSocket ?? baseOptions.MaxConcurrentResubscriptionsPerSocket;
SocketResponseTimeout = newValues?.SocketResponseTimeout ?? baseOptions.SocketResponseTimeout;
SocketNoDataTimeout = newValues?.SocketNoDataTimeout ?? baseOptions.SocketNoDataTimeout;
SocketSubscriptionsCombineTarget = newValues?.SocketSubscriptionsCombineTarget ?? baseOptions.SocketSubscriptionsCombineTarget;
MaxSocketConnections = newValues?.MaxSocketConnections ?? baseOptions.MaxSocketConnections;
DelayAfterConnect = newValues?.DelayAfterConnect ?? baseOptions.DelayAfterConnect;
}
/// <inheritdoc />
@@ -304,14 +304,14 @@ namespace CryptoExchange.Net.OrderBook
}
/// <inheritdoc/>
public CallResult<decimal> CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type)
public CallResult<decimal> CalculateAverageFillPrice(decimal baseQuantity, OrderBookEntryType type)
{
if (Status != OrderBookStatus.Synced)
return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state"));
var totalCost = 0m;
var totalAmount = 0m;
var amountLeft = quantity;
var amountLeft = baseQuantity;
lock (_bookLock)
{
var list = type == OrderBookEntryType.Ask ? asks : bids;
@@ -334,6 +334,35 @@ namespace CryptoExchange.Net.OrderBook
return new CallResult<decimal>(Math.Round(totalCost / totalAmount, 8));
}
/// <inheritdoc/>
public CallResult<decimal> CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type)
{
if (Status != OrderBookStatus.Synced)
return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state"));
var quoteQuantityLeft = quoteQuantity;
var totalBaseQuantity = 0m;
lock (_bookLock)
{
var list = type == OrderBookEntryType.Ask ? asks : bids;
var step = 0;
while (quoteQuantityLeft > 0)
{
if (step == list.Count)
return new CallResult<decimal>(new InvalidOperationError("Quantity is larger than order in the order book"));
var element = list.ElementAt(step);
var stepAmount = Math.Min(element.Value.Quantity * element.Value.Price, quoteQuantityLeft);
quoteQuantityLeft -= stepAmount;
totalBaseQuantity += stepAmount / element.Value.Price;
step++;
}
}
return new CallResult<decimal>(Math.Round(totalBaseQuantity, 8));
}
/// <summary>
/// Implementation for starting the order book. Should typically have logic for subscribing to the update stream and retrieving
/// and setting the initial order book
@@ -211,8 +211,16 @@ namespace CryptoExchange.Net.Sockets
while (_closeTask == null)
await Task.Delay(50).ConfigureAwait(false);
await _closeTask.ConfigureAwait(false);
_closeTask = null;
await _closeSem.WaitAsync().ConfigureAwait(false);
try
{
await _closeTask.ConfigureAwait(false);
_closeTask = null;
}
finally
{
_closeSem.Release();
}
if (!Parameters.AutoReconnect)
{
@@ -285,8 +293,19 @@ namespace CryptoExchange.Net.Sockets
return;
_log.Write(LogLevel.Debug, $"Socket {Id} reconnect requested");
_closeTask = CloseInternalAsync();
await _closeTask.ConfigureAwait(false);
await _closeSem.WaitAsync().ConfigureAwait(false);
try
{
if (_processState != ProcessState.Processing)
return;
_closeTask = CloseInternalAsync();
await _closeTask.ConfigureAwait(false);
}
finally
{
_closeSem.Release();
}
}
/// <inheritdoc />
@@ -430,8 +449,19 @@ namespace CryptoExchange.Net.Sockets
{
// Connection closed unexpectedly, .NET framework
OnError?.Invoke(ioe);
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
await _closeSem.WaitAsync().ConfigureAwait(false);
try
{
if (_processState != ProcessState.Processing)
return;
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
}
catch
{
_closeSem.Release();
}
break;
}
}
@@ -488,8 +518,17 @@ namespace CryptoExchange.Net.Sockets
{
// Connection closed unexpectedly
OnError?.Invoke(wse);
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
await _closeSem.WaitAsync().ConfigureAwait(false);
try
{
if (_processState == ProcessState.Processing && _closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
}
catch
{
_closeSem.Release();
}
break;
}
@@ -497,8 +536,17 @@ namespace CryptoExchange.Net.Sockets
{
// Connection closed unexpectedly
_log.Write(LogLevel.Debug, $"Socket {Id} received `Close` message");
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
await _closeSem.WaitAsync().ConfigureAwait(false);
try
{
if (_processState == ProcessState.Processing && _closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
}
catch
{
_closeSem.Release();
}
break;
}
+13 -9
View File
@@ -11,15 +11,19 @@ namespace CryptoExchange.Net.Sockets
public JToken? Result { get; private set; }
public bool Completed { get; private set; }
public AsyncResetEvent Event { get; }
public DateTime RequestTimestamp { get; set; }
public TimeSpan Timeout { get; }
public SocketSubscription? Subscription { get; }
private CancellationTokenSource cts;
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout)
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription)
{
Handler = handler;
Event = new AsyncResetEvent(false, false);
Timeout = timeout;
RequestTimestamp = DateTime.UtcNow;
Subscription = subscription;
cts = new CancellationTokenSource(timeout);
cts.Token.Register(Fail, false);
@@ -27,15 +31,15 @@ namespace CryptoExchange.Net.Sockets
public bool CheckData(JToken data)
{
if (Handler(data))
{
Result = data;
Completed = true;
Event.Set();
return true;
}
return Handler(data);
}
return false;
public bool Succeed(JToken data)
{
Result = data;
Completed = true;
Event.Set();
return true;
}
public void Fail()
+22 -4
View File
@@ -253,7 +253,7 @@ namespace CryptoExchange.Net.Sockets
var reconnectSuccessful = await ProcessReconnectAsync().ConfigureAwait(false);
if (!reconnectSuccessful)
{
_log.Write(LogLevel.Warning, "Failed reconnect processing, reconnecting again");
_log.Write(LogLevel.Warning, $"Failed reconnect processing: {reconnectSuccessful.Error}, reconnecting again");
await _socket.ReconnectAsync().ConfigureAwait(false);
}
else
@@ -304,18 +304,35 @@ namespace CryptoExchange.Net.Sockets
PendingRequest[] requests;
lock (_pendingRequests)
{
_pendingRequests.RemoveAll(r => r.Completed);
// Remove only timed out requests after 5 minutes have passed so we can still process any
// message coming in after the request timeout
_pendingRequests.RemoveAll(r => r.Completed && DateTime.UtcNow - r.RequestTimestamp > TimeSpan.FromMinutes(5));
requests = _pendingRequests.ToArray();
}
// Check if this message is an answer on any pending requests
foreach (var pendingRequest in requests)
{
if (pendingRequest.CheckData(tokenData))
{
lock (_pendingRequests)
_pendingRequests.Remove(pendingRequest);
if (pendingRequest.Completed)
{
// Answer to a timed out request, unsub if it is a subscription request
if (pendingRequest.Subscription != null)
{
_log.Write(LogLevel.Warning, "Received subscription info after request timed out; unsubscribing. Consider increasing the SocketResponseTimout");
_ = ApiClient.UnsubscribeAsync(this, pendingRequest.Subscription).ConfigureAwait(false);
}
}
else
{
pendingRequest.Succeed(tokenData);
}
if (!ApiClient.ContinueOnQueryResponse)
return;
@@ -546,11 +563,12 @@ namespace CryptoExchange.Net.Sockets
/// <typeparam name="T">The data type expected in response</typeparam>
/// <param name="obj">The object to send</param>
/// <param name="timeout">The timeout for response</param>
/// <param name="subscription">Subscription if this is a subscribe request</param>
/// <param name="handler">The response handler, should return true if the received JToken was the response to the request</param>
/// <returns></returns>
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, Func<JToken, bool> handler)
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, SocketSubscription? subscription, Func<JToken, bool> handler)
{
var pending = new PendingRequest(handler, timeout);
var pending = new PendingRequest(handler, timeout, subscription);
lock (_pendingRequests)
{
_pendingRequests.Add(pending);
+11
View File
@@ -33,6 +33,17 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes
* Version 5.4.1 - 18 Mar 2023
* Added CalculateTradableAmount to SymbolOrderBook
* Improved socket reconnect robustness
* Fixed api rate limiter not working correctly
* Version 5.4.0 - 14 Feb 2023
* Added unsubscribing when receiving subscribe answer after the request timeout has passed
* Fixed socket options copying
* Made TimeSync implementation optional
* Cleaned up ApiCredentials and added better support for extending ApiCredentials
* Version 5.3.1 - 08 Dec 2022
* Added default request parameter ordering before applying authentication
* Fixed possible issue where a socket would reconnect when it should close if it was already in reconnecting