mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-13 17:33:02 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b34129e148 | |||
| be25a68c9c | |||
| 468cd5e48e | |||
| 262c4e4aa5 | |||
| 4ccff6461f | |||
| 3bfa3ef389 | |||
| 5238971bcc | |||
| 2f5c904faf | |||
| c62775813f | |||
| f11b3754f0 | |||
| 3cbe0465e9 | |||
| 5048aea722 | |||
| 8d35339ab2 | |||
| c3316a51e7 | |||
| 7ecf37064b | |||
| 18954f4f53 | |||
| 00bc245102 | |||
| 273cab9fdb |
@@ -140,6 +140,12 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
||||||
client.SubClient.ConnectSocketSub(sub1);
|
client.SubClient.ConnectSocketSub(sub1);
|
||||||
client.SubClient.ConnectSocketSub(sub2);
|
client.SubClient.ConnectSocketSub(sub2);
|
||||||
|
var us1 = SocketSubscription.CreateForIdentifier(10, "Test1", true, false, (e) => { });
|
||||||
|
var us2 = SocketSubscription.CreateForIdentifier(11, "Test2", true, false, (e) => { });
|
||||||
|
sub1.AddSubscription(us1);
|
||||||
|
sub2.AddSubscription(us2);
|
||||||
|
var ups1 = new UpdateSubscription(sub1, us1);
|
||||||
|
var ups2 = new UpdateSubscription(sub2, us2);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
client.UnsubscribeAllAsync().Wait();
|
client.UnsubscribeAllAsync().Wait();
|
||||||
|
|||||||
@@ -182,9 +182,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Error ParseErrorResponse(JToken error)
|
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
||||||
{
|
{
|
||||||
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
|
var errorData = ValidateJson(data);
|
||||||
|
|
||||||
|
return new ServerError((int)errorData.Data["errorCode"], (string)errorData.Data["errorMessage"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSpan? GetTimeOffset()
|
public override TimeSpan? GetTimeOffset()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
public event Action OnReconnected;
|
public event Action OnReconnected;
|
||||||
public event Action OnReconnecting;
|
public event Action OnReconnecting;
|
||||||
#pragma warning restore 0067
|
#pragma warning restore 0067
|
||||||
|
public event Action<int> OnRequestSent;
|
||||||
public event Action<string> OnMessage;
|
public event Action<string> OnMessage;
|
||||||
public event Action<Exception> OnError;
|
public event Action<Exception> OnError;
|
||||||
public event Action OnOpen;
|
public event Action OnOpen;
|
||||||
@@ -69,10 +70,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
return Task.FromResult(CanConnect);
|
return Task.FromResult(CanConnect);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Send(string data)
|
public void Send(int requestId, string data, int weight)
|
||||||
{
|
{
|
||||||
if(!Connected)
|
if(!Connected)
|
||||||
throw new Exception("Socket not connected");
|
throw new Exception("Socket not connected");
|
||||||
|
OnRequestSent?.Invoke(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
|
|||||||
@@ -77,15 +77,6 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool OutputOriginalData { get; }
|
public bool OutputOriginalData { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The last used id, use NextId() to get the next id and up this
|
|
||||||
/// </summary>
|
|
||||||
protected static int _lastId;
|
|
||||||
/// <summary>
|
|
||||||
/// Lock for id generating
|
|
||||||
/// </summary>
|
|
||||||
protected static object _idLock = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A default serializer
|
/// A default serializer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -338,19 +329,6 @@ namespace CryptoExchange.Net
|
|||||||
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static int NextId()
|
|
||||||
{
|
|
||||||
lock (_idLock)
|
|
||||||
{
|
|
||||||
_lastId += 1;
|
|
||||||
return _lastId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Diagnostics;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -14,6 +15,7 @@ using CryptoExchange.Net.Requests;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using static CryptoExchange.Net.Objects.RateLimiter;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@@ -71,7 +73,7 @@ namespace CryptoExchange.Net
|
|||||||
rateLimiters.Add(rateLimiter);
|
rateLimiters.Add(rateLimiter);
|
||||||
RateLimiters = rateLimiters;
|
RateLimiters = rateLimiters;
|
||||||
|
|
||||||
RequestFactory.Configure(options.RequestTimeout, httpClient);
|
RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -194,7 +196,7 @@ namespace CryptoExchange.Net
|
|||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
bool ignoreRatelimit = false)
|
bool ignoreRatelimit = false)
|
||||||
{
|
{
|
||||||
var requestId = NextId();
|
var requestId = ExchangeHelpers.NextId();
|
||||||
|
|
||||||
if (signed)
|
if (signed)
|
||||||
{
|
{
|
||||||
@@ -344,8 +346,13 @@ namespace CryptoExchange.Net
|
|||||||
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
_logger.Log(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
||||||
responseStream.Close();
|
responseStream.Close();
|
||||||
response.Close();
|
response.Close();
|
||||||
var parseResult = ValidateJson(data);
|
|
||||||
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!;
|
Error error;
|
||||||
|
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
|
||||||
|
error = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, data);
|
||||||
|
else
|
||||||
|
error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, data);
|
||||||
|
|
||||||
if (error.Code == null || error.Code == 0)
|
if (error.Code == null || error.Code == 0)
|
||||||
error.Code = (int)response.StatusCode;
|
error.Code = (int)response.StatusCode;
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data.Length, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||||
@@ -529,13 +536,39 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse an error response from the server. Only used when server returns a status other than Success(200)
|
/// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">The string the request returned</param>
|
/// <param name="httpStatusCode">The response status code</param>
|
||||||
|
/// <param name="responseHeaders">The response headers</param>
|
||||||
|
/// <param name="data">The response data</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Error ParseErrorResponse(JToken error)
|
protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
||||||
{
|
{
|
||||||
return new ServerError(error.ToString());
|
return new ServerError(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpStatusCode">The response status code</param>
|
||||||
|
/// <param name="responseHeaders">The response headers</param>
|
||||||
|
/// <param name="data">The response data</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
||||||
|
{
|
||||||
|
// Handle retry after header
|
||||||
|
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
if (!retryAfterHeader.Value.Any())
|
||||||
|
return new ServerRateLimitError(data);
|
||||||
|
|
||||||
|
var value = retryAfterHeader.Value.First();
|
||||||
|
if (int.TryParse(value, out var seconds))
|
||||||
|
return new ServerRateLimitError(data) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
|
||||||
|
|
||||||
|
if (DateTime.TryParse(value, out var datetime))
|
||||||
|
return new ServerRateLimitError(data) { RetryAfter = datetime };
|
||||||
|
|
||||||
|
return new ServerRateLimitError(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static CryptoExchange.Net.Objects.RateLimiter;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@@ -76,9 +77,9 @@ namespace CryptoExchange.Net
|
|||||||
protected internal bool UnhandledMessageExpected { get; set; }
|
protected internal bool UnhandledMessageExpected { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The max amount of outgoing messages per socket per second
|
/// The rate limiters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal int? RateLimitPerSocketPerSecond { get; set; }
|
protected internal IEnumerable<IRateLimiter>? RateLimiters { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public double IncomingKbps
|
||||||
@@ -130,6 +131,10 @@ namespace CryptoExchange.Net
|
|||||||
options,
|
options,
|
||||||
apiOptions)
|
apiOptions)
|
||||||
{
|
{
|
||||||
|
var rateLimiters = new List<IRateLimiter>();
|
||||||
|
foreach (var rateLimiter in apiOptions.RateLimiters)
|
||||||
|
rateLimiters.Add(rateLimiter);
|
||||||
|
RateLimiters = rateLimiters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -275,7 +280,7 @@ namespace CryptoExchange.Net
|
|||||||
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
|
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
|
||||||
{
|
{
|
||||||
CallResult<object>? callResult = null;
|
CallResult<object>? callResult = null;
|
||||||
await socketConnection.SendAndWaitAsync(request, ClientOptions.RequestTimeout, subscription, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
await socketConnection.SendAndWaitAsync(request, ClientOptions.RequestTimeout, subscription, 1, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
|
||||||
|
|
||||||
if (callResult?.Success == true)
|
if (callResult?.Success == true)
|
||||||
{
|
{
|
||||||
@@ -295,10 +300,11 @@ namespace CryptoExchange.Net
|
|||||||
/// <typeparam name="T">Expected result type</typeparam>
|
/// <typeparam name="T">Expected result type</typeparam>
|
||||||
/// <param name="request">The request to send, will be serialized to json</param>
|
/// <param name="request">The request to send, will be serialized to json</param>
|
||||||
/// <param name="authenticated">If the query is to an authenticated endpoint</param>
|
/// <param name="authenticated">If the query is to an authenticated endpoint</param>
|
||||||
|
/// <param name="weight">Weight of the request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated)
|
protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated, int weight = 1)
|
||||||
{
|
{
|
||||||
return QueryAsync<T>(BaseAddress, request, authenticated);
|
return QueryAsync<T>(BaseAddress, request, authenticated, weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -308,8 +314,9 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="url">The url for the request</param>
|
/// <param name="url">The url for the request</param>
|
||||||
/// <param name="request">The request to send</param>
|
/// <param name="request">The request to send</param>
|
||||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
|
/// <param name="weight">Weight of the request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated)
|
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated, int weight = 1)
|
||||||
{
|
{
|
||||||
if (_disposing)
|
if (_disposing)
|
||||||
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
|
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
|
||||||
@@ -348,7 +355,7 @@ namespace CryptoExchange.Net
|
|||||||
return new CallResult<T>(new ServerError("Socket is paused"));
|
return new CallResult<T>(new ServerError("Socket is paused"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return await QueryAndWaitAsync<T>(socketConnection, request).ConfigureAwait(false);
|
return await QueryAndWaitAsync<T>(socketConnection, request, weight).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -357,11 +364,12 @@ namespace CryptoExchange.Net
|
|||||||
/// <typeparam name="T">The expected result type</typeparam>
|
/// <typeparam name="T">The expected result type</typeparam>
|
||||||
/// <param name="socket">The connection to send and wait on</param>
|
/// <param name="socket">The connection to send and wait on</param>
|
||||||
/// <param name="request">The request to send</param>
|
/// <param name="request">The request to send</param>
|
||||||
|
/// <param name="weight">The weight of the query</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request)
|
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request, int weight)
|
||||||
{
|
{
|
||||||
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
|
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
|
||||||
await socket.SendAndWaitAsync(request, ClientOptions.RequestTimeout, null, data =>
|
await socket.SendAndWaitAsync(request, ClientOptions.RequestTimeout, null, weight, data =>
|
||||||
{
|
{
|
||||||
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
|
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
|
||||||
return false;
|
return false;
|
||||||
@@ -518,8 +526,8 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
var subscription = request == null
|
var subscription = request == null
|
||||||
? SocketSubscription.CreateForIdentifier(NextId(), identifier!, userSubscription, authenticated, InternalHandler)
|
? SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), identifier!, userSubscription, authenticated, InternalHandler)
|
||||||
: SocketSubscription.CreateForRequest(NextId(), request, userSubscription, authenticated, InternalHandler);
|
: SocketSubscription.CreateForRequest(ExchangeHelpers.NextId(), request, userSubscription, authenticated, InternalHandler);
|
||||||
if (!connection.AddSubscription(subscription))
|
if (!connection.AddSubscription(subscription))
|
||||||
return null;
|
return null;
|
||||||
return subscription;
|
return subscription;
|
||||||
@@ -533,7 +541,7 @@ namespace CryptoExchange.Net
|
|||||||
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
|
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
|
||||||
{
|
{
|
||||||
genericHandlers.Add(identifier, action);
|
genericHandlers.Add(identifier, action);
|
||||||
var subscription = SocketSubscription.CreateForIdentifier(NextId(), identifier, false, false, action);
|
var subscription = SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), identifier, false, false, action);
|
||||||
foreach (var connection in socketConnections.Values)
|
foreach (var connection in socketConnections.Values)
|
||||||
connection.AddSubscription(subscription);
|
connection.AddSubscription(subscription);
|
||||||
}
|
}
|
||||||
@@ -607,7 +615,7 @@ namespace CryptoExchange.Net
|
|||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||||
foreach (var kvp in genericHandlers)
|
foreach (var kvp in genericHandlers)
|
||||||
{
|
{
|
||||||
var handler = SocketSubscription.CreateForIdentifier(NextId(), kvp.Key, false, false, kvp.Value);
|
var handler = SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), kvp.Key, false, false, kvp.Value);
|
||||||
socketConnection.AddSubscription(handler);
|
socketConnection.AddSubscription(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,7 +659,7 @@ namespace CryptoExchange.Net
|
|||||||
DataInterpreterString = dataInterpreterString,
|
DataInterpreterString = dataInterpreterString,
|
||||||
KeepAliveInterval = KeepAliveInterval,
|
KeepAliveInterval = KeepAliveInterval,
|
||||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||||
RatelimitPerSecond = RateLimitPerSocketPerSecond,
|
RateLimiters = RateLimiters,
|
||||||
Proxy = ClientOptions.Proxy,
|
Proxy = ClientOptions.Proxy,
|
||||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
|
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout
|
||||||
};
|
};
|
||||||
@@ -704,7 +712,7 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
socketConnection.Send(obj);
|
socketConnection.Send(ExchangeHelpers.NextId(), obj, 1);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -762,6 +770,10 @@ namespace CryptoExchange.Net
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task UnsubscribeAllAsync()
|
public virtual async Task UnsubscribeAllAsync()
|
||||||
{
|
{
|
||||||
|
var sum = socketConnections.Sum(s => s.Value.SubscriptionCount);
|
||||||
|
if (sum == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
_logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
_logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,16 +6,16 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||||
<PackageVersion>6.0.0</PackageVersion>
|
<PackageVersion>6.1.0</PackageVersion>
|
||||||
<AssemblyVersion>6.0.0</AssemblyVersion>
|
<AssemblyVersion>6.1.0</AssemblyVersion>
|
||||||
<FileVersion>6.0.0</FileVersion>
|
<FileVersion>6.1.0</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<PackageReleaseNotes>6.0.0 - Updated ApiCredentials to support RSA signing as well as the default Hmac signature, Removed custom logging implementation in favor of using `Microsoft.Extensions.Logging` ILogger directly, Refactored client options for easier use, Added easier way of switching environments, Added ResponseLength and ToString() override on WebCallResult object, Fixed memory leak in AsyncResetEvent</PackageReleaseNotes>
|
<PackageReleaseNotes>6.1.0 - Added support for ratelimiting on socket connections, Added rest ratelimit handling and parsing, Added ServerRatelimitError error</PackageReleaseNotes>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>10.0</LangVersion>
|
<LangVersion>10.0</LangVersion>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ExchangeHelpers
|
public static class ExchangeHelpers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The last used id, use NextId() to get the next id and up this
|
||||||
|
/// </summary>
|
||||||
|
private static int _lastId;
|
||||||
|
/// <summary>
|
||||||
|
/// Lock for id generating
|
||||||
|
/// </summary>
|
||||||
|
private static object _idLock = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clamp a value between a min and max
|
/// Clamp a value between a min and max
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -118,5 +127,19 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
return value / 1.000000000000000000000000000000000m;
|
return value / 1.000000000000000000000000000000000m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static int NextId()
|
||||||
|
{
|
||||||
|
lock (_idLock)
|
||||||
|
{
|
||||||
|
_lastId += 1;
|
||||||
|
return _lastId;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using System;
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requestTimeout">Request timeout to use</param>
|
/// <param name="requestTimeout">Request timeout to use</param>
|
||||||
/// <param name="httpClient">Optional shared http client instance</param>
|
/// <param name="httpClient">Optional shared http client instance</param>
|
||||||
void Configure(TimeSpan requestTimeout, HttpClient? httpClient=null);
|
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
|
||||||
|
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using System.Threading.Tasks;
|
|||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Webscoket connection interface
|
/// Websocket connection interface
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IWebsocket: IDisposable
|
public interface IWebsocket: IDisposable
|
||||||
{
|
{
|
||||||
@@ -21,6 +21,10 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
event Action<string> OnMessage;
|
event Action<string> OnMessage;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Websocket sent event, RequestId as parameter
|
||||||
|
/// </summary>
|
||||||
|
event Action<int> OnRequestSent;
|
||||||
|
/// <summary>
|
||||||
/// Websocket error event
|
/// Websocket error event
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Action<Exception> OnError;
|
event Action<Exception> OnError;
|
||||||
@@ -69,8 +73,10 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send data
|
/// Send data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
void Send(string data);
|
/// <param name="weight"></param>
|
||||||
|
void Send(int id, string data, int weight);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reconnect the socket
|
/// Reconnect the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace CryptoExchange.Net.Objects
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for errors
|
/// Base class for errors
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{Code}: {Message} {Data}";
|
return Code != null ? $"{Code}: {Message} {Data}" : $"{Message} {Data}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +54,14 @@
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CantConnectError() : base(null, "Can't connect to the server", null) { }
|
public CantConnectError() : base(null, "Can't connect to the server", null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected CantConnectError(int? code, string message, object? data) : base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -63,12 +73,20 @@
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { }
|
public NoApiCredentialsError() : base(null, "No credentials provided for private endpoint", null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected NoApiCredentialsError(int? code, string message, object? data) : base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Error returned by the server
|
/// Error returned by the server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ServerError: Error
|
public class ServerError : Error
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -83,9 +101,15 @@
|
|||||||
/// <param name="code"></param>
|
/// <param name="code"></param>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
public ServerError(int code, string message, object? data = null) : base(code, message, data)
|
public ServerError(int code, string message, object? data = null) : base(code, message, data) { }
|
||||||
{
|
|
||||||
}
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected ServerError(int? code, string message, object? data) : base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -107,6 +131,14 @@
|
|||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
public WebError(int code, string message, object? data = null) : base(code, message, data) { }
|
public WebError(int code, string message, object? data = null) : base(code, message, data) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected WebError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -120,6 +152,14 @@
|
|||||||
/// <param name="message">The error message</param>
|
/// <param name="message">The error message</param>
|
||||||
/// <param name="data">The data which caused the error</param>
|
/// <param name="data">The data which caused the error</param>
|
||||||
public DeserializeError(string message, object? data) : base(null, message, data) { }
|
public DeserializeError(string message, object? data) : base(null, message, data) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected DeserializeError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -133,6 +173,14 @@
|
|||||||
/// <param name="message">Error message</param>
|
/// <param name="message">Error message</param>
|
||||||
/// <param name="data">Error data</param>
|
/// <param name="data">Error data</param>
|
||||||
public UnknownError(string message, object? data = null) : base(null, message, data) { }
|
public UnknownError(string message, object? data = null) : base(null, message, data) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected UnknownError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -145,18 +193,73 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { }
|
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected ArgumentError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rate limit exceeded
|
/// Rate limit exceeded (client side)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RateLimitError: Error
|
public abstract class BaseRateLimitError : Error
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// When the request can be retried
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RetryAfter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected BaseRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit exceeded (client side)
|
||||||
|
/// </summary>
|
||||||
|
public class ClientRateLimitError : BaseRateLimitError
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
public RateLimitError(string message) : base(null, "Rate limit exceeded: " + message, null) { }
|
public ClientRateLimitError(string message) : base(null, "Client rate limit exceeded: " + message, null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected ClientRateLimitError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rate limit exceeded (server side)
|
||||||
|
/// </summary>
|
||||||
|
public class ServerRateLimitError : BaseRateLimitError
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
public ServerRateLimitError(string message) : base(null, "Server rate limit exceeded: " + message, null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected ServerRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -168,17 +271,33 @@
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
|
public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invalid operation requested
|
/// Invalid operation requested
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class InvalidOperationError: Error
|
public class InvalidOperationError : Error
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
public InvalidOperationError(string message) : base(null, message, null) { }
|
public InvalidOperationError(string message) : base(null, message, null) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <param name="data"></param>
|
||||||
|
protected InvalidOperationError(int? code, string message, object? data): base(code, message, data) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,329 +0,0 @@
|
|||||||
//using System;
|
|
||||||
//using System.Collections.Generic;
|
|
||||||
//using System.Linq;
|
|
||||||
//using System.Net.Http;
|
|
||||||
//using CryptoExchange.Net.Authentication;
|
|
||||||
//using CryptoExchange.Net.Interfaces;
|
|
||||||
//using CryptoExchange.Net.Logging;
|
|
||||||
//using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
//namespace CryptoExchange.Net.Objects
|
|
||||||
//{
|
|
||||||
// /// <summary>
|
|
||||||
// /// Client options
|
|
||||||
// /// </summary>
|
|
||||||
// public abstract class ClientOptions
|
|
||||||
// {
|
|
||||||
// internal event Action? OnLoggingChanged;
|
|
||||||
|
|
||||||
// private LogLevel _logLevel = LogLevel.Information;
|
|
||||||
// /// <summary>
|
|
||||||
// /// The minimum log level to output
|
|
||||||
// /// </summary>
|
|
||||||
// public LogLevel LogLevel
|
|
||||||
// {
|
|
||||||
// get => _logLevel;
|
|
||||||
// set
|
|
||||||
// {
|
|
||||||
// _logLevel = value;
|
|
||||||
// OnLoggingChanged?.Invoke();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private List<ILogger> _logWriters = new List<ILogger> { new DebugLogger() };
|
|
||||||
// /// <summary>
|
|
||||||
// /// The log writers
|
|
||||||
// /// </summary>
|
|
||||||
// public List<ILogger> LogWriters
|
|
||||||
// {
|
|
||||||
// get => _logWriters;
|
|
||||||
// set
|
|
||||||
// {
|
|
||||||
// _logWriters = value;
|
|
||||||
// OnLoggingChanged?.Invoke();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// Proxy to use when connecting
|
|
||||||
// /// </summary>
|
|
||||||
// public ApiProxy? Proxy { get; set; }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// The api credentials used for signing requests to this API.
|
|
||||||
// /// </summary>
|
|
||||||
// public ApiCredentials? ApiCredentials { get; set; }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// ctor
|
|
||||||
// /// </summary>
|
|
||||||
// public ClientOptions()
|
|
||||||
// {
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// ctor
|
|
||||||
// /// </summary>
|
|
||||||
// /// <param name="clientOptions">Copy values for the provided options</param>
|
|
||||||
// public ClientOptions(ClientOptions? clientOptions)
|
|
||||||
// {
|
|
||||||
// if (clientOptions == null)
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// LogLevel = clientOptions.LogLevel;
|
|
||||||
// LogWriters = clientOptions.LogWriters.ToList();
|
|
||||||
// Proxy = clientOptions.Proxy;
|
|
||||||
// ApiCredentials = clientOptions.ApiCredentials?.Copy();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// ctor
|
|
||||||
// /// </summary>
|
|
||||||
// /// <param name="baseOptions">Copy values for the provided options</param>
|
|
||||||
// /// <param name="newValues">Copy values for the provided options</param>
|
|
||||||
// internal ClientOptions(ClientOptions baseOptions, ClientOptions? newValues)
|
|
||||||
// {
|
|
||||||
// Proxy = newValues?.Proxy ?? baseOptions.Proxy;
|
|
||||||
// LogLevel = baseOptions.LogLevel;
|
|
||||||
// LogWriters = baseOptions.LogWriters.ToList();
|
|
||||||
// ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <inheritdoc />
|
|
||||||
// public override string ToString()
|
|
||||||
// {
|
|
||||||
// return $"LogLevel: {LogLevel}, Writers: {LogWriters.Count}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}";
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// API client options
|
|
||||||
// /// </summary>
|
|
||||||
// public class ApiClientOptions
|
|
||||||
// {
|
|
||||||
// /// <summary>
|
|
||||||
// /// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property
|
|
||||||
// /// </summary>
|
|
||||||
// public bool OutputOriginalData { get; set; } = false;
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// The base address of the API
|
|
||||||
// /// </summary>
|
|
||||||
// public string BaseAddress { get; set; }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// The api credentials used for signing requests to this API. Overrides API credentials provided in the client options
|
|
||||||
// /// </summary>
|
|
||||||
// public ApiCredentials? ApiCredentials { get; set; }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// ctor
|
|
||||||
// /// </summary>
|
|
||||||
//#pragma warning disable 8618 // Will always get filled by the implementation
|
|
||||||
// public ApiClientOptions()
|
|
||||||
// {
|
|
||||||
// }
|
|
||||||
//#pragma warning restore 8618
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// ctor
|
|
||||||
// /// </summary>
|
|
||||||
// /// <param name="baseAddress">Base address for the API</param>
|
|
||||||
// public ApiClientOptions(string baseAddress)
|
|
||||||
// {
|
|
||||||
// BaseAddress = baseAddress;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// ctor
|
|
||||||
// /// </summary>
|
|
||||||
// /// <param name="baseOptions">Copy values for the provided options</param>
|
|
||||||
// /// <param name="newValues">Copy values for the provided options</param>
|
|
||||||
// public ApiClientOptions(ApiClientOptions baseOptions, ApiClientOptions? newValues)
|
|
||||||
// {
|
|
||||||
// BaseAddress = newValues?.BaseAddress ?? baseOptions.BaseAddress;
|
|
||||||
// ApiCredentials = newValues?.ApiCredentials?.Copy() ?? baseOptions.ApiCredentials?.Copy();
|
|
||||||
// OutputOriginalData = newValues?.OutputOriginalData ?? baseOptions.OutputOriginalData;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <inheritdoc />
|
|
||||||
// public override string ToString()
|
|
||||||
// {
|
|
||||||
// return $"OutputOriginalData: {OutputOriginalData}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}";
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// Rest API client options
|
|
||||||
// /// </summary>
|
|
||||||
// public class RestApiClientOptions: ApiClientOptions
|
|
||||||
// {
|
|
||||||
// /// <summary>
|
|
||||||
// /// The time the server has to respond to a request before timing out
|
|
||||||
// /// </summary>
|
|
||||||
// public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options provided in these options will be ignored in requests and should be set on the provided HttpClient instance
|
|
||||||
// /// </summary>
|
|
||||||
// public HttpClient? HttpClient { get; set; }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// List of rate limiters to use
|
|
||||||
// /// </summary>
|
|
||||||
// public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// What to do when a call would exceed the rate limit
|
|
||||||
// /// </summary>
|
|
||||||
// public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// Whether or not to automatically sync the local time with the server time
|
|
||||||
// /// </summary>
|
|
||||||
// public bool AutoTimestamp { get; set; }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
|
||||||
// /// </summary>
|
|
||||||
// public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// ctor
|
|
||||||
// /// </summary>
|
|
||||||
// public RestApiClientOptions()
|
|
||||||
// {
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// ctor
|
|
||||||
// /// </summary>
|
|
||||||
// /// <param name="baseAddress">Base address for the API</param>
|
|
||||||
// public RestApiClientOptions(string baseAddress): base(baseAddress)
|
|
||||||
// {
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// ctor
|
|
||||||
// /// </summary>
|
|
||||||
// /// <param name="baseOn">Copy values for the provided options</param>
|
|
||||||
// /// <param name="newValues">Copy values for the provided options</param>
|
|
||||||
// public RestApiClientOptions(RestApiClientOptions baseOn, RestApiClientOptions? newValues): base(baseOn, newValues)
|
|
||||||
// {
|
|
||||||
// HttpClient = newValues?.HttpClient ?? baseOn.HttpClient;
|
|
||||||
// RequestTimeout = newValues == default ? baseOn.RequestTimeout : newValues.RequestTimeout;
|
|
||||||
// RateLimitingBehaviour = newValues?.RateLimitingBehaviour ?? baseOn.RateLimitingBehaviour;
|
|
||||||
// AutoTimestamp = newValues?.AutoTimestamp ?? baseOn.AutoTimestamp;
|
|
||||||
// TimestampRecalculationInterval = newValues?.TimestampRecalculationInterval ?? baseOn.TimestampRecalculationInterval;
|
|
||||||
// RateLimiters = newValues?.RateLimiters.ToList() ?? baseOn?.RateLimiters.ToList() ?? new List<IRateLimiter>();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <inheritdoc />
|
|
||||||
// public override string ToString()
|
|
||||||
// {
|
|
||||||
// return $"{base.ToString()}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-" : "set")}, RateLimiters: {RateLimiters?.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, AutoTimestamp: {AutoTimestamp}, TimestampRecalculationInterval: {TimestampRecalculationInterval}";
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// Rest API client options
|
|
||||||
// /// </summary>
|
|
||||||
// public class SocketApiClientOptions : ApiClientOptions
|
|
||||||
// {
|
|
||||||
// /// <summary>
|
|
||||||
// /// Whether or not the socket should automatically reconnect when losing connection
|
|
||||||
// /// </summary>
|
|
||||||
// public bool AutoReconnect { get; set; } = true;
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// Time to wait between reconnect attempts
|
|
||||||
// /// </summary>
|
|
||||||
// public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
|
||||||
// /// </summary>
|
|
||||||
// public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// The max time to wait for a response after sending a request on the socket before giving a timeout
|
|
||||||
// /// </summary>
|
|
||||||
// public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
|
||||||
// /// for example when the server sends intermittent ping requests
|
|
||||||
// /// </summary>
|
|
||||||
// public TimeSpan SocketNoDataTimeout { get; set; }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// The amount of subscriptions that should be made on a single socket connection. Not all API's support multiple subscriptions on a single socket.
|
|
||||||
// /// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a
|
|
||||||
// /// single connection will also increase the amount of traffic on that single connection, potentially leading to issues.
|
|
||||||
// /// </summary>
|
|
||||||
// public int? SocketSubscriptionsCombineTarget { get; set; }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// The max amount of connections to make to the server. Can be used for API's which only allow a certain number of connections. Changing this to a high value might cause issues.
|
|
||||||
// /// </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; } = TimeSpan.Zero;
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// ctor
|
|
||||||
// /// </summary>
|
|
||||||
// public SocketApiClientOptions()
|
|
||||||
// {
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// ctor
|
|
||||||
// /// </summary>
|
|
||||||
// /// <param name="baseAddress">Base address for the API</param>
|
|
||||||
// public SocketApiClientOptions(string baseAddress) : base(baseAddress)
|
|
||||||
// {
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// ctor
|
|
||||||
// /// </summary>
|
|
||||||
// /// <param name="baseOptions">Copy values for the provided options</param>
|
|
||||||
// /// <param name="newValues">Copy values for the provided options</param>
|
|
||||||
// public SocketApiClientOptions(SocketApiClientOptions baseOptions, SocketApiClientOptions? newValues) : base(baseOptions, newValues)
|
|
||||||
// {
|
|
||||||
// if (baseOptions == null)
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// 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 />
|
|
||||||
// public override string ToString()
|
|
||||||
// {
|
|
||||||
// return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}, MaxSocketConnections: {MaxSocketConnections}";
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// <summary>
|
|
||||||
// /// Base for order book options
|
|
||||||
// /// </summary>
|
|
||||||
// public class OrderBookOptions : ClientOptions
|
|
||||||
// {
|
|
||||||
// /// <summary>
|
|
||||||
// /// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
|
||||||
// /// </summary>
|
|
||||||
// public bool ChecksumValidationEnabled { get; set; } = true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
//}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Options
|
namespace CryptoExchange.Net.Objects.Options
|
||||||
{
|
{
|
||||||
@@ -8,6 +10,11 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SocketApiOptions : ApiOptions
|
public class SocketApiOptions : ApiOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// List of rate limiters to use
|
||||||
|
/// </summary>
|
||||||
|
public List<IRateLimiter> RateLimiters { get; set; } = new List<IRateLimiter>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
/// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected,
|
||||||
/// for example when the server sends intermittent ping requests
|
/// for example when the server sends intermittent ping requests
|
||||||
@@ -30,6 +37,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
{
|
{
|
||||||
ApiCredentials = ApiCredentials?.Copy(),
|
ApiCredentials = ApiCredentials?.Copy(),
|
||||||
OutputOriginalData = OutputOriginalData,
|
OutputOriginalData = OutputOriginalData,
|
||||||
|
RateLimiters = RateLimiters,
|
||||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||||
MaxSocketConnections = MaxSocketConnections,
|
MaxSocketConnections = MaxSocketConnections,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public class RateLimiter : IRateLimiter
|
public class RateLimiter : IRateLimiter
|
||||||
{
|
{
|
||||||
private readonly object _limiterLock = new object();
|
private readonly object _limiterLock = new object();
|
||||||
internal List<Limiter> Limiters = new List<Limiter>();
|
internal List<Limiter> _limiters = new List<Limiter>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>,
|
/// Create a new RateLimiter. Configure the rate limiter by calling <see cref="AddTotalRateLimit"/>,
|
||||||
@@ -35,7 +35,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
|
public RateLimiter AddTotalRateLimit(int limit, TimeSpan perTimePeriod)
|
||||||
{
|
{
|
||||||
lock(_limiterLock)
|
lock(_limiterLock)
|
||||||
Limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
|
_limiters.Add(new TotalRateLimiter(limit, perTimePeriod, null));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
public RateLimiter AddEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
||||||
{
|
{
|
||||||
lock(_limiterLock)
|
lock(_limiterLock)
|
||||||
Limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
_limiters.Add(new EndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
public RateLimiter AddEndpointLimit(IEnumerable<string> endpoints, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool excludeFromOtherRateLimits = false)
|
||||||
{
|
{
|
||||||
lock(_limiterLock)
|
lock(_limiterLock)
|
||||||
Limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
_limiters.Add(new EndpointRateLimiter(endpoints.ToArray(), limit, perTimePeriod, method, excludeFromOtherRateLimits));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
|
public RateLimiter AddPartialEndpointLimit(string endpoint, int limit, TimeSpan perTimePeriod, HttpMethod? method = null, bool countPerEndpoint = false, bool ignoreOtherRateLimits = false)
|
||||||
{
|
{
|
||||||
lock(_limiterLock)
|
lock(_limiterLock)
|
||||||
Limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
|
_limiters.Add(new PartialEndpointRateLimiter(new[] { endpoint }, limit, perTimePeriod, method, ignoreOtherRateLimits, countPerEndpoint));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,20 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
|
public RateLimiter AddApiKeyLimit(int limit, TimeSpan perTimePeriod, bool onlyForSignedRequests, bool excludeFromTotalRateLimit)
|
||||||
{
|
{
|
||||||
lock(_limiterLock)
|
lock(_limiterLock)
|
||||||
Limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
|
_limiters.Add(new ApiKeyRateLimiter(limit, perTimePeriod, null, onlyForSignedRequests, excludeFromTotalRateLimit));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a rate limit for the amount of messages that can be send per connection
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="endpoint">The endpoint that the limit is for</param>
|
||||||
|
/// <param name="limit">The limit per period. Note that this is weight, not single request, altough by default requests have a weight of 1</param>
|
||||||
|
/// <param name="perTimePeriod">The time period the limit is for</param>
|
||||||
|
public RateLimiter AddConnectionRateLimit(string endpoint, int limit, TimeSpan perTimePeriod)
|
||||||
|
{
|
||||||
|
lock (_limiterLock)
|
||||||
|
_limiters.Add(new ConnectionRateLimiter(new[] { endpoint }, limit, perTimePeriod));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +119,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
|
|
||||||
EndpointRateLimiter? endpointLimit;
|
EndpointRateLimiter? endpointLimit;
|
||||||
lock (_limiterLock)
|
lock (_limiterLock)
|
||||||
endpointLimit = Limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
|
endpointLimit = _limiters.OfType<EndpointRateLimiter>().SingleOrDefault(h => h.Endpoints.Contains(endpoint) && (h.Method == null || h.Method == method));
|
||||||
if(endpointLimit != null)
|
if(endpointLimit != null)
|
||||||
{
|
{
|
||||||
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
var waitResult = await ProcessTopic(logger, endpointLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||||
@@ -121,7 +134,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
|
|
||||||
List<PartialEndpointRateLimiter> partialEndpointLimits;
|
List<PartialEndpointRateLimiter> partialEndpointLimits;
|
||||||
lock (_limiterLock)
|
lock (_limiterLock)
|
||||||
partialEndpointLimits = Limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
|
partialEndpointLimits = _limiters.OfType<PartialEndpointRateLimiter>().Where(h => h.PartialEndpoints.Any(h => endpoint.Contains(h)) && (h.Method == null || h.Method == method)).ToList();
|
||||||
foreach (var partialEndpointLimit in partialEndpointLimits)
|
foreach (var partialEndpointLimit in partialEndpointLimits)
|
||||||
{
|
{
|
||||||
if (partialEndpointLimit.CountPerEndpoint)
|
if (partialEndpointLimit.CountPerEndpoint)
|
||||||
@@ -129,11 +142,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
SingleTopicRateLimiter? thisEndpointLimit;
|
SingleTopicRateLimiter? thisEndpointLimit;
|
||||||
lock (_limiterLock)
|
lock (_limiterLock)
|
||||||
{
|
{
|
||||||
thisEndpointLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
|
thisEndpointLimit = _limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.PartialEndpoint && (string)h.Topic == endpoint);
|
||||||
if (thisEndpointLimit == null)
|
if (thisEndpointLimit == null)
|
||||||
{
|
{
|
||||||
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
|
thisEndpointLimit = new SingleTopicRateLimiter(endpoint, partialEndpointLimit);
|
||||||
Limiters.Add(thisEndpointLimit);
|
_limiters.Add(thisEndpointLimit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +171,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
|
|
||||||
ApiKeyRateLimiter? apiLimit;
|
ApiKeyRateLimiter? apiLimit;
|
||||||
lock (_limiterLock)
|
lock (_limiterLock)
|
||||||
apiLimit = Limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey);
|
apiLimit = _limiters.OfType<ApiKeyRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey);
|
||||||
if (apiLimit != null)
|
if (apiLimit != null)
|
||||||
{
|
{
|
||||||
if(apiKey == null)
|
if(apiKey == null)
|
||||||
@@ -177,11 +190,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
SingleTopicRateLimiter? thisApiLimit;
|
SingleTopicRateLimiter? thisApiLimit;
|
||||||
lock (_limiterLock)
|
lock (_limiterLock)
|
||||||
{
|
{
|
||||||
thisApiLimit = Limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
|
thisApiLimit = _limiters.OfType<SingleTopicRateLimiter>().SingleOrDefault(h => h.Type == RateLimitType.ApiKey && ((SecureString)h.Topic).IsEqualTo(apiKey));
|
||||||
if (thisApiLimit == null)
|
if (thisApiLimit == null)
|
||||||
{
|
{
|
||||||
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
|
thisApiLimit = new SingleTopicRateLimiter(apiKey, apiLimit);
|
||||||
Limiters.Add(thisApiLimit);
|
_limiters.Add(thisApiLimit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +211,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
|
|
||||||
TotalRateLimiter? totalLimit;
|
TotalRateLimiter? totalLimit;
|
||||||
lock (_limiterLock)
|
lock (_limiterLock)
|
||||||
totalLimit = Limiters.OfType<TotalRateLimiter>().SingleOrDefault();
|
totalLimit = _limiters.OfType<TotalRateLimiter>().SingleOrDefault();
|
||||||
if (totalLimit != null)
|
if (totalLimit != null)
|
||||||
{
|
{
|
||||||
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
var waitResult = await ProcessTopic(logger, totalLimit, endpoint, requestWeight, limitBehaviour, ct).ConfigureAwait(false);
|
||||||
@@ -224,63 +237,68 @@ namespace CryptoExchange.Net.Objects
|
|||||||
}
|
}
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
|
|
||||||
int totalWaitTime = 0;
|
try
|
||||||
while (true)
|
|
||||||
{
|
{
|
||||||
// Remove requests no longer in time period from the history
|
int totalWaitTime = 0;
|
||||||
var checkTime = DateTime.UtcNow;
|
while (true)
|
||||||
for (var i = 0; i < historyTopic.Entries.Count; i++)
|
|
||||||
{
|
{
|
||||||
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
|
// Remove requests no longer in time period from the history
|
||||||
|
var checkTime = DateTime.UtcNow;
|
||||||
|
for (var i = 0; i < historyTopic.Entries.Count; i++)
|
||||||
{
|
{
|
||||||
historyTopic.Entries.Remove(historyTopic.Entries[i]);
|
if (historyTopic.Entries[i].Timestamp < checkTime - historyTopic.Period)
|
||||||
i--;
|
{
|
||||||
|
historyTopic.Entries.Remove(historyTopic.Entries[i]);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentWeight = !historyTopic.Entries.Any() ? 0 : historyTopic.Entries.Sum(h => h.Weight);
|
||||||
|
if (currentWeight + requestWeight > historyTopic.Limit)
|
||||||
|
{
|
||||||
|
if (currentWeight == 0)
|
||||||
|
throw new Exception("Request limit reached without any prior request. " +
|
||||||
|
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}");
|
||||||
|
|
||||||
|
// Wait until the next entry should be removed from the history
|
||||||
|
var thisWaitTime = (int)Math.Round(((historyTopic.Entries.First().Timestamp + historyTopic.Period) - checkTime).TotalMilliseconds);
|
||||||
|
if (thisWaitTime > 0)
|
||||||
|
{
|
||||||
|
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
||||||
|
{
|
||||||
|
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
|
||||||
|
logger.Log(LogLevel.Warning, msg);
|
||||||
|
return new CallResult<int>(new ClientRateLimitError(msg) { RetryAfter = DateTime.UtcNow.AddSeconds(thisWaitTime) });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log(LogLevel.Information, $"Message to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return new CallResult<int>(new CancellationRequestedError());
|
||||||
|
}
|
||||||
|
totalWaitTime += thisWaitTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentWeight = !historyTopic.Entries.Any() ? 0: historyTopic.Entries.Sum(h => h.Weight);
|
|
||||||
if (currentWeight + requestWeight > historyTopic.Limit)
|
|
||||||
{
|
|
||||||
if (currentWeight == 0)
|
|
||||||
throw new Exception("Request limit reached without any prior request. " +
|
|
||||||
$"This request can never execute with the current rate limiter. Request weight: {requestWeight}, Ratelimit: {historyTopic.Limit}");
|
|
||||||
|
|
||||||
// Wait until the next entry should be removed from the history
|
|
||||||
var thisWaitTime = (int)Math.Round((historyTopic.Entries.First().Timestamp - (checkTime - historyTopic.Period)).TotalMilliseconds);
|
|
||||||
if (thisWaitTime > 0)
|
|
||||||
{
|
{
|
||||||
if (limitBehaviour == RateLimitingBehaviour.Fail)
|
break;
|
||||||
{
|
|
||||||
historyTopic.Semaphore.Release();
|
|
||||||
var msg = $"Request to {endpoint} failed because of rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}";
|
|
||||||
logger.Log(LogLevel.Warning, msg);
|
|
||||||
return new CallResult<int>(new RateLimitError(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Log(LogLevel.Information, $"Request to {endpoint} waiting {thisWaitTime}ms for rate limit `{historyTopic.Type}`. Current weight: {currentWeight}/{historyTopic.Limit}, request weight: {requestWeight}");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(thisWaitTime, ct).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
return new CallResult<int>(new CancellationRequestedError());
|
|
||||||
}
|
|
||||||
totalWaitTime += thisWaitTime;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newTime = DateTime.UtcNow;
|
var newTime = DateTime.UtcNow;
|
||||||
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
|
historyTopic.Entries.Add(new LimitEntry(newTime, requestWeight));
|
||||||
historyTopic.Semaphore.Release();
|
return new CallResult<int>(totalWaitTime);
|
||||||
return new CallResult<int>(totalWaitTime);
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
historyTopic.Semaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal struct LimitEntry
|
internal struct LimitEntry
|
||||||
@@ -329,6 +347,24 @@ namespace CryptoExchange.Net.Objects
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class ConnectionRateLimiter : PartialEndpointRateLimiter
|
||||||
|
{
|
||||||
|
public ConnectionRateLimiter(int limit, TimeSpan perPeriod)
|
||||||
|
: base(new[] { "/" }, limit, perPeriod, null, true, true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConnectionRateLimiter(string[] endpoints, int limit, TimeSpan perPeriod)
|
||||||
|
: base(endpoints, limit, perPeriod, null, true, true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return nameof(ConnectionRateLimiter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal class EndpointRateLimiter: Limiter
|
internal class EndpointRateLimiter: Limiter
|
||||||
{
|
{
|
||||||
public string[] Endpoints { get; set; }
|
public string[] Endpoints { get; set; }
|
||||||
|
|||||||
@@ -9,8 +9,19 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TraceLoggerProvider : ILoggerProvider
|
public class TraceLoggerProvider : ILoggerProvider
|
||||||
{
|
{
|
||||||
|
private readonly LogLevel _logLevel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logLevel"></param>
|
||||||
|
public TraceLoggerProvider(LogLevel? logLevel = null)
|
||||||
|
{
|
||||||
|
_logLevel = logLevel ?? LogLevel.Trace;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName);
|
public ILogger CreateLogger(string categoryName) => new TraceLogger(categoryName, _logLevel);
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose() { }
|
public void Dispose() { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Requests
|
namespace CryptoExchange.Net.Requests
|
||||||
{
|
{
|
||||||
@@ -14,12 +16,27 @@ namespace CryptoExchange.Net.Requests
|
|||||||
private HttpClient? _httpClient;
|
private HttpClient? _httpClient;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Configure(TimeSpan requestTimeout, HttpClient? client = null)
|
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
|
||||||
{
|
{
|
||||||
_httpClient = client ?? new HttpClient()
|
if (client == null)
|
||||||
{
|
{
|
||||||
Timeout = requestTimeout
|
var handler = new HttpClientHandler();
|
||||||
};
|
if (proxy != null)
|
||||||
|
{
|
||||||
|
handler.Proxy = new WebProxy
|
||||||
|
{
|
||||||
|
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
||||||
|
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
client = new HttpClient(handler)
|
||||||
|
{
|
||||||
|
Timeout = requestTimeout
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_httpClient = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -30,9 +31,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
private static readonly object _streamIdLock = new();
|
private static readonly object _streamIdLock = new();
|
||||||
|
|
||||||
private readonly AsyncResetEvent _sendEvent;
|
private readonly AsyncResetEvent _sendEvent;
|
||||||
private readonly ConcurrentQueue<byte[]> _sendBuffer;
|
private readonly ConcurrentQueue<SendItem> _sendBuffer;
|
||||||
private readonly SemaphoreSlim _closeSem;
|
private readonly SemaphoreSlim _closeSem;
|
||||||
private readonly List<DateTime> _outgoingMessages;
|
|
||||||
|
|
||||||
private ClientWebSocket _socket;
|
private ClientWebSocket _socket;
|
||||||
private CancellationTokenSource _ctsSource;
|
private CancellationTokenSource _ctsSource;
|
||||||
@@ -103,6 +103,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action<string>? OnMessage;
|
public event Action<string>? OnMessage;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public event Action<int>? OnRequestSent;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action<Exception>? OnError;
|
public event Action<Exception>? OnError;
|
||||||
|
|
||||||
@@ -128,10 +131,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
Parameters = websocketParameters;
|
Parameters = websocketParameters;
|
||||||
_outgoingMessages = new List<DateTime>();
|
|
||||||
_receivedMessages = new List<ReceiveItem>();
|
_receivedMessages = new List<ReceiveItem>();
|
||||||
_sendEvent = new AsyncResetEvent();
|
_sendEvent = new AsyncResetEvent();
|
||||||
_sendBuffer = new ConcurrentQueue<byte[]>();
|
_sendBuffer = new ConcurrentQueue<SendItem>();
|
||||||
_ctsSource = new CancellationTokenSource();
|
_ctsSource = new CancellationTokenSource();
|
||||||
_receivedMessagesLock = new object();
|
_receivedMessagesLock = new object();
|
||||||
|
|
||||||
@@ -270,14 +272,14 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void Send(string data)
|
public virtual void Send(int id, string data, int weight)
|
||||||
{
|
{
|
||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var bytes = Parameters.Encoding.GetBytes(data);
|
var bytes = Parameters.Encoding.GetBytes(data);
|
||||||
_logger.Log(LogLevel.Trace, $"Socket {Id} Adding {bytes.Length} to sent buffer");
|
_logger.Log(LogLevel.Trace, $"Socket {Id} - msg {id} - Adding {bytes.Length} to send buffer");
|
||||||
_sendBuffer.Enqueue(bytes);
|
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
|
||||||
_sendEvent.Set();
|
_sendEvent.Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,6 +394,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var limitKey = Uri.ToString() + "/" + Id.ToString();
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
@@ -404,25 +407,24 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
while (_sendBuffer.TryDequeue(out var data))
|
while (_sendBuffer.TryDequeue(out var data))
|
||||||
{
|
{
|
||||||
if (Parameters.RatelimitPerSecond != null)
|
if (Parameters.RateLimiters != null)
|
||||||
{
|
{
|
||||||
// Wait for rate limit
|
foreach(var ratelimiter in Parameters.RateLimiters)
|
||||||
DateTime? start = null;
|
|
||||||
while (MessagesSentLastSecond() >= Parameters.RatelimitPerSecond)
|
|
||||||
{
|
{
|
||||||
start ??= DateTime.UtcNow;
|
var limitResult = await ratelimiter.LimitRequestAsync(_logger, limitKey, HttpMethod.Get, false, null, RateLimitingBehaviour.Wait, data.Weight, _ctsSource.Token).ConfigureAwait(false);
|
||||||
await Task.Delay(50).ConfigureAwait(false);
|
if (limitResult.Success)
|
||||||
|
{
|
||||||
|
if (limitResult.Data > 0)
|
||||||
|
_logger.Log(LogLevel.Debug, $"Socket {Id} - msg {data.Id} - send delayed {limitResult.Data}ms because of rate limit");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start != null)
|
|
||||||
_logger.Log(LogLevel.Debug, $"Socket {Id} sent delayed {Math.Round((DateTime.UtcNow - start.Value).TotalMilliseconds)}ms because of rate limit");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _socket.SendAsync(new ArraySegment<byte>(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
|
||||||
_outgoingMessages.Add(DateTime.UtcNow);
|
OnRequestSent?.Invoke(data.Id);
|
||||||
_logger.Log(LogLevel.Trace, $"Socket {Id} sent {data.Length} bytes");
|
_logger.Log(LogLevel.Trace, $"Socket {Id} - msg {data.Id} - sent {data.Bytes.Length} bytes");
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -630,42 +632,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger the OnMessage event
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected void TriggerOnMessage(string data)
|
|
||||||
{
|
|
||||||
LastActionTime = DateTime.UtcNow;
|
|
||||||
OnMessage?.Invoke(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger the OnError event
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ex"></param>
|
|
||||||
protected void TriggerOnError(Exception ex) => OnError?.Invoke(ex);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger the OnError event
|
|
||||||
/// </summary>
|
|
||||||
protected void TriggerOnOpen() => OnOpen?.Invoke();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger the OnError event
|
|
||||||
/// </summary>
|
|
||||||
protected void TriggerOnClose() => OnClose?.Invoke();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger the OnReconnecting event
|
|
||||||
/// </summary>
|
|
||||||
protected void TriggerOnReconnecting() => OnReconnecting?.Invoke();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trigger the OnReconnected event
|
|
||||||
/// </summary>
|
|
||||||
protected void TriggerOnReconnected() => OnReconnected?.Invoke();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if there is no data received for a period longer than the specified timeout
|
/// Checks if there is no data received for a period longer than the specified timeout
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -721,13 +687,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int MessagesSentLastSecond()
|
|
||||||
{
|
|
||||||
var testTime = DateTime.UtcNow;
|
|
||||||
_outgoingMessages.RemoveAll(r => testTime - r > TimeSpan.FromSeconds(1));
|
|
||||||
return _outgoingMessages.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the received messages list, removing messages received longer than 3s ago
|
/// Update the received messages list, removing messages received longer than 3s ago
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -769,6 +728,32 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message info
|
||||||
|
/// </summary>
|
||||||
|
public struct SendItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The request id
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The request id
|
||||||
|
/// </summary>
|
||||||
|
public int Weight { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp the request was sent
|
||||||
|
/// </summary>
|
||||||
|
public DateTime SendTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The bytes to send
|
||||||
|
/// </summary>
|
||||||
|
public byte[] Bytes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Received message info
|
/// Received message info
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
internal class PendingRequest
|
internal class PendingRequest
|
||||||
{
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
public Func<JToken, bool> Handler { get; }
|
public Func<JToken, bool> Handler { get; }
|
||||||
public JToken? Result { get; private set; }
|
public JToken? Result { get; private set; }
|
||||||
public bool Completed { get; private set; }
|
public bool Completed { get; private set; }
|
||||||
@@ -15,17 +16,22 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
public TimeSpan Timeout { get; }
|
public TimeSpan Timeout { get; }
|
||||||
public SocketSubscription? Subscription { get; }
|
public SocketSubscription? Subscription { get; }
|
||||||
|
|
||||||
private CancellationTokenSource _cts;
|
private CancellationTokenSource? _cts;
|
||||||
|
|
||||||
public PendingRequest(Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription)
|
public PendingRequest(int id, Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription)
|
||||||
{
|
{
|
||||||
|
Id = id;
|
||||||
Handler = handler;
|
Handler = handler;
|
||||||
Event = new AsyncResetEvent(false, false);
|
Event = new AsyncResetEvent(false, false);
|
||||||
Timeout = timeout;
|
Timeout = timeout;
|
||||||
RequestTimestamp = DateTime.UtcNow;
|
RequestTimestamp = DateTime.UtcNow;
|
||||||
Subscription = subscription;
|
Subscription = subscription;
|
||||||
|
}
|
||||||
|
|
||||||
_cts = new CancellationTokenSource(timeout);
|
public void IsSend()
|
||||||
|
{
|
||||||
|
// Start timeout countdown
|
||||||
|
_cts = new CancellationTokenSource(Timeout);
|
||||||
_cts.Token.Register(Fail, false);
|
_cts.Token.Register(Fail, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// Tag for identificaion
|
/// Tag for identificaion
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Tag { get; set; }
|
public string Tag { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional properties for this connection
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, object> Properties { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If activity is paused
|
/// If activity is paused
|
||||||
@@ -170,12 +175,14 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
ApiClient = apiClient;
|
ApiClient = apiClient;
|
||||||
Tag = tag;
|
Tag = tag;
|
||||||
|
Properties = new Dictionary<string, object>();
|
||||||
|
|
||||||
_pendingRequests = new List<PendingRequest>();
|
_pendingRequests = new List<PendingRequest>();
|
||||||
_subscriptions = new List<SocketSubscription>();
|
_subscriptions = new List<SocketSubscription>();
|
||||||
|
|
||||||
_socket = socket;
|
_socket = socket;
|
||||||
_socket.OnMessage += HandleMessage;
|
_socket.OnMessage += HandleMessage;
|
||||||
|
_socket.OnRequestSent += HandleRequestSent;
|
||||||
_socket.OnOpen += HandleOpen;
|
_socket.OnOpen += HandleOpen;
|
||||||
_socket.OnClose += HandleClose;
|
_socket.OnClose += HandleClose;
|
||||||
_socket.OnReconnecting += HandleReconnecting;
|
_socket.OnReconnecting += HandleReconnecting;
|
||||||
@@ -278,6 +285,22 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_logger.Log(LogLevel.Warning, $"Socket {SocketId} error: " + e.ToLogString());
|
_logger.Log(LogLevel.Warning, $"Socket {SocketId} error: " + e.ToLogString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler for whenever a request is sent over the websocket
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestId">Id of the request sent</param>
|
||||||
|
protected virtual void HandleRequestSent(int requestId)
|
||||||
|
{
|
||||||
|
var pendingRequest = _pendingRequests.SingleOrDefault(p => p.Id == requestId);
|
||||||
|
if (pendingRequest == null)
|
||||||
|
{
|
||||||
|
_logger.Log(LogLevel.Debug, $"Socket {SocketId} - msg {requestId} - message sent, but not pending");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingRequest.IsSend();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process a message received by the socket
|
/// Process a message received by the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -312,7 +335,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Check if this message is an answer on any pending requests
|
// Check if this message is an answer on any pending requests
|
||||||
foreach (var pendingRequest in requests)
|
foreach (var pendingRequest in requests)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (pendingRequest.CheckData(tokenData))
|
if (pendingRequest.CheckData(tokenData))
|
||||||
{
|
{
|
||||||
lock (_pendingRequests)
|
lock (_pendingRequests)
|
||||||
@@ -323,12 +345,13 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Answer to a timed out request, unsub if it is a subscription request
|
// Answer to a timed out request, unsub if it is a subscription request
|
||||||
if (pendingRequest.Subscription != null)
|
if (pendingRequest.Subscription != null)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Warning, "Received subscription info after request timed out; unsubscribing. Consider increasing the SocketResponseTimout");
|
_logger.Log(LogLevel.Warning, "Received subscription info after request timed out; unsubscribing. Consider increasing the RequestTimeout");
|
||||||
_ = ApiClient.UnsubscribeAsync(this, pendingRequest.Subscription).ConfigureAwait(false);
|
_ = ApiClient.UnsubscribeAsync(this, pendingRequest.Subscription).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
_logger.Log(LogLevel.Trace, $"Socket {SocketId} - msg {pendingRequest.Id} - received data matched to pending request");
|
||||||
pendingRequest.Succeed(tokenData);
|
pendingRequest.Succeed(tokenData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,45 +587,69 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="timeout">The timeout for response</param>
|
/// <param name="timeout">The timeout for response</param>
|
||||||
/// <param name="subscription">Subscription if this is a subscribe request</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>
|
/// <param name="handler">The response handler, should return true if the received JToken was the response to the request</param>
|
||||||
|
/// <param name="weight">The weight of the message</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, SocketSubscription? subscription, Func<JToken, bool> handler)
|
public virtual async Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, SocketSubscription? subscription, int weight, Func<JToken, bool> handler)
|
||||||
{
|
{
|
||||||
var pending = new PendingRequest(handler, timeout, subscription);
|
var pending = new PendingRequest(ExchangeHelpers.NextId(), handler, timeout, subscription);
|
||||||
lock (_pendingRequests)
|
lock (_pendingRequests)
|
||||||
{
|
{
|
||||||
_pendingRequests.Add(pending);
|
_pendingRequests.Add(pending);
|
||||||
}
|
}
|
||||||
var sendOk = Send(obj);
|
|
||||||
if(!sendOk)
|
|
||||||
pending.Fail();
|
|
||||||
|
|
||||||
return pending.Event.WaitAsync(timeout);
|
var sendOk = Send(pending.Id, obj, weight);
|
||||||
|
if (!sendOk)
|
||||||
|
{
|
||||||
|
pending.Fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if(!_socket.IsOpen)
|
||||||
|
{
|
||||||
|
pending.Fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.Completed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await pending.Event.WaitAsync(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (pending.Completed)
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send data over the websocket connection
|
/// Send data over the websocket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of the object to send</typeparam>
|
/// <typeparam name="T">The type of the object to send</typeparam>
|
||||||
|
/// <param name="requestId">The request id</param>
|
||||||
/// <param name="obj">The object to send</param>
|
/// <param name="obj">The object to send</param>
|
||||||
/// <param name="nullValueHandling">How null values should be serialized</param>
|
/// <param name="nullValueHandling">How null values should be serialized</param>
|
||||||
public virtual bool Send<T>(T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
|
/// <param name="weight">The weight of the message</param>
|
||||||
|
public virtual bool Send<T>(int requestId, T obj, int weight, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
|
||||||
{
|
{
|
||||||
if(obj is string str)
|
if(obj is string str)
|
||||||
return Send(str);
|
return Send(requestId, str, weight);
|
||||||
else
|
else
|
||||||
return Send(JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }));
|
return Send(requestId, JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }), weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send string data over the websocket connection
|
/// Send string data over the websocket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">The data to send</param>
|
/// <param name="data">The data to send</param>
|
||||||
public virtual bool Send(string data)
|
/// <param name="weight">The weight of the message</param>
|
||||||
|
/// <param name="requestId">The id of the request</param>
|
||||||
|
public virtual bool Send(int requestId, string data, int weight)
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Trace, $"Socket {SocketId} sending data: {data}");
|
_logger.Log(LogLevel.Trace, $"Socket {SocketId} - msg {requestId} - sending messsage: {data}");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_socket.Send(data);
|
_socket.Send(requestId, data, weight);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch(Exception)
|
catch(Exception)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -52,9 +53,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
public TimeSpan? KeepAliveInterval { get; set; }
|
public TimeSpan? KeepAliveInterval { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The max amount of messages to send per second
|
/// The rate limiters for the socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? RatelimitPerSecond { get; set; }
|
public IEnumerable<IRateLimiter>? RateLimiters { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Origin header value to send in the connection handshake
|
/// Origin header value to send in the connection handshake
|
||||||
|
|||||||
@@ -5,20 +5,15 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="8.0.6" />
|
<PackageReference Include="Binance.Net" Version="9.0.1" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="5.0.3" />
|
<PackageReference Include="Bitfinex.Net" Version="6.0.0" />
|
||||||
<PackageReference Include="Bittrex.Net" Version="7.0.4" />
|
<PackageReference Include="Bittrex.Net" Version="8.0.0" />
|
||||||
<PackageReference Include="Bybit.Net" Version="0.0.4" />
|
<PackageReference Include="Bybit.Net" Version="3.0.0" />
|
||||||
<PackageReference Include="CoinEx.Net" Version="5.0.3" />
|
<PackageReference Include="CoinEx.Net" Version="6.0.0" />
|
||||||
<PackageReference Include="FTX.Net" Version="1.0.4" />
|
<PackageReference Include="Huobi.Net" Version="5.0.0" />
|
||||||
<PackageReference Include="Huobi.Net" Version="4.0.4" />
|
<PackageReference Include="KrakenExchange.Net" Version="4.0.0" />
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="3.0.3" />
|
<PackageReference Include="Kucoin.Net" Version="5.0.0" />
|
||||||
<PackageReference Include="Kucoin.Net" Version="4.0.3" />
|
<PackageReference Include="Serilog.AspNetCore" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.1-dev-00250" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Data\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@inject IBinanceClient binanceClient
|
@inject IBinanceRestClient binanceClient
|
||||||
@inject IBitfinexClient bitfinexClient
|
@inject IBitfinexRestClient bitfinexClient
|
||||||
@inject IBittrexClient bittrexClient
|
@inject IBittrexRestClient bittrexClient
|
||||||
@inject IBybitClient bybitClient
|
@inject IBybitRestClient bybitClient
|
||||||
@inject ICoinExClient coinexClient
|
@inject ICoinExRestClient coinexClient
|
||||||
@inject IFTXClient ftxClient
|
@inject IHuobiRestClient huobiClient
|
||||||
@inject IHuobiClient huobiClient
|
@inject IKrakenRestClient krakenClient
|
||||||
@inject IKrakenClient krakenClient
|
@inject IKucoinRestClient kucoinClient
|
||||||
@inject IKucoinClient kucoinClient
|
|
||||||
|
|
||||||
<h3>BTC-USD prices:</h3>
|
<h3>BTC-USD prices:</h3>
|
||||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||||
@@ -23,14 +22,13 @@
|
|||||||
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||||
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
||||||
var bittrexTask = bittrexClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
var bittrexTask = bittrexClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||||
var bybitTask = bybitClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
|
||||||
var coinexTask = coinexClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
var coinexTask = coinexClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||||
var ftxTask = ftxClient.TradeApi.ExchangeData.GetSymbolAsync("BTC/USD");
|
|
||||||
var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
|
var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
|
||||||
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
|
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
|
||||||
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||||
|
|
||||||
await Task.WhenAll(binanceTask, bitfinexTask, bittrexTask, bybitTask, coinexTask, ftxTask, huobiTask, krakenTask, kucoinTask);
|
await Task.WhenAll(binanceTask, bitfinexTask, bittrexTask, bybitTask, coinexTask, huobiTask, krakenTask, kucoinTask);
|
||||||
|
|
||||||
if (binanceTask.Result.Success)
|
if (binanceTask.Result.Success)
|
||||||
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
|
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
|
||||||
@@ -42,14 +40,11 @@
|
|||||||
_prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice);
|
_prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice);
|
||||||
|
|
||||||
if (bybitTask.Result.Success)
|
if (bybitTask.Result.Success)
|
||||||
_prices.Add("Bybit", bybitTask.Result.Data.LastPrice);
|
_prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);
|
||||||
|
|
||||||
if (coinexTask.Result.Success)
|
if (coinexTask.Result.Success)
|
||||||
_prices.Add("CoinEx", coinexTask.Result.Data.Ticker.LastPrice);
|
_prices.Add("CoinEx", coinexTask.Result.Data.Ticker.LastPrice);
|
||||||
|
|
||||||
if (ftxTask.Result.Success)
|
|
||||||
_prices.Add("FTX", ftxTask.Result.Data.LastPrice ?? 0);
|
|
||||||
|
|
||||||
if (huobiTask.Result.Success)
|
if (huobiTask.Result.Success)
|
||||||
_prices.Add("Huobi", huobiTask.Result.Data.ClosePrice ?? 0);
|
_prices.Add("Huobi", huobiTask.Result.Data.ClosePrice ?? 0);
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,12 @@
|
|||||||
@inject IBittrexSocketClient bittrexSocketClient
|
@inject IBittrexSocketClient bittrexSocketClient
|
||||||
@inject IBybitSocketClient bybitSocketClient
|
@inject IBybitSocketClient bybitSocketClient
|
||||||
@inject ICoinExSocketClient coinExSocketClient
|
@inject ICoinExSocketClient coinExSocketClient
|
||||||
@inject IFTXSocketClient ftxSocketClient
|
|
||||||
@inject IHuobiSocketClient huobiSocketClient
|
@inject IHuobiSocketClient huobiSocketClient
|
||||||
@inject IKrakenSocketClient krakenSocketClient
|
@inject IKrakenSocketClient krakenSocketClient
|
||||||
@inject IKucoinSocketClient kucoinSocketClient
|
@inject IKucoinSocketClient kucoinSocketClient
|
||||||
@using Binance.Net.Clients.SpotApi
|
@using System.Collections.Concurrent
|
||||||
@using Bitfinex.Net.Clients.SpotApi
|
|
||||||
@using Bittrex.Net.Clients.SpotApi
|
|
||||||
@using Bybit.Net.Clients.SpotApi
|
|
||||||
@using CoinEx.Net.Clients.SpotApi
|
|
||||||
@using CryptoExchange.Net.Objects
|
@using CryptoExchange.Net.Objects
|
||||||
@using CryptoExchange.Net.Sockets
|
@using CryptoExchange.Net.Sockets
|
||||||
@using Huobi.Net.Clients.SpotApi
|
|
||||||
@using Kraken.Net.Clients.SpotApi
|
|
||||||
@using Kucoin.Net.Clients.SpotApi
|
|
||||||
@using System.Collections.Concurrent
|
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
<h3>ETH-BTC prices, live updates:</h3>
|
<h3>ETH-BTC prices, live updates:</h3>
|
||||||
@@ -35,15 +26,14 @@
|
|||||||
{
|
{
|
||||||
var tasks = new Task<CallResult<UpdateSubscription>>[]
|
var tasks = new Task<CallResult<UpdateSubscription>>[]
|
||||||
{
|
{
|
||||||
binanceSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
|
||||||
bitfinexSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
||||||
bittrexSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)),
|
bittrexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)),
|
||||||
bybitSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
||||||
coinExSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
|
coinExSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
|
||||||
ftxSocketClient.Streams.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("FTX", data.Data.LastPrice ?? 0)),
|
huobiSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
|
||||||
huobiSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
|
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
|
||||||
krakenSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
|
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
||||||
kucoinSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
await Task.WhenAll(tasks);
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
@page "/OrderBooks"
|
@page "/OrderBooks"
|
||||||
@using Binance.Net.SymbolOrderBooks
|
|
||||||
@using Bitfinex.Net.SymbolOrderBooks
|
|
||||||
@using Bittrex.Net.SymbolOrderBooks
|
|
||||||
@using Bybit.Net.SymbolOrderBooks
|
|
||||||
@using CryptoExchange.Net.Interfaces
|
|
||||||
@using CryptoExchange.Net.Objects
|
|
||||||
@using CryptoExchange.Net.Sockets
|
|
||||||
@using CoinEx.Net.SymbolOrderBooks
|
|
||||||
@using FTX.Net.SymbolOrderBooks
|
|
||||||
@using Huobi.Net.SymbolOrderBooks
|
|
||||||
@using Kraken.Net.SymbolOrderBooks
|
|
||||||
@using Kucoin.Net.Clients
|
|
||||||
@using Kucoin.Net.SymbolOrderBooks
|
|
||||||
@using System.Collections.Concurrent
|
@using System.Collections.Concurrent
|
||||||
@using System.Timers
|
@using System.Timers
|
||||||
|
@using Binance.Net.Interfaces
|
||||||
|
@using Bitfinex.Net.Interfaces
|
||||||
|
@using Bittrex.Net.Interfaces
|
||||||
|
@using Bybit.Net.Interfaces
|
||||||
|
@using CoinEx.Net.Interfaces
|
||||||
|
@using CryptoExchange.Net.Interfaces
|
||||||
|
@using Huobi.Net.Interfaces
|
||||||
|
@using Kraken.Net.Interfaces
|
||||||
|
@using Kucoin.Net.Clients
|
||||||
|
@using Kucoin.Net.Interfaces
|
||||||
|
@inject IBinanceOrderBookFactory binanceFactory
|
||||||
|
@inject IBitfinexOrderBookFactory bitfinexFactory
|
||||||
|
@inject IBittrexOrderBookFactory bittrexFactory
|
||||||
|
@inject IBybitOrderBookFactory bybitFactory
|
||||||
|
@inject ICoinExOrderBookFactory coinExFactory
|
||||||
|
@inject IHuobiOrderBookFactory huobiFactory
|
||||||
|
@inject IKrakenOrderBookFactory krakenFactory
|
||||||
|
@inject IKucoinOrderBookFactory kucoinFactory
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
<h3>ETH-BTC books, live updates:</h3>
|
<h3>ETH-BTC books, live updates:</h3>
|
||||||
@@ -40,22 +45,21 @@
|
|||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
// Since the Kucoin order book stream needs authentication we will need to provide API credentials beforehand
|
// Since the Kucoin order book stream needs authentication we will need to provide API credentials beforehand
|
||||||
KucoinClient.SetDefaultOptions(new Kucoin.Net.Objects.KucoinClientOptions
|
KucoinRestClient.SetDefaultOptions(options =>
|
||||||
{
|
{
|
||||||
ApiCredentials = new Kucoin.Net.Objects.KucoinApiCredentials("KEY", "SECRET", "PASSPHRASE")
|
options.ApiCredentials = new Kucoin.Net.Objects.KucoinApiCredentials("KEY", "SECRET", "PASSPHRASE");
|
||||||
});
|
});
|
||||||
|
|
||||||
_books = new Dictionary<string, ISymbolOrderBook>
|
_books = new Dictionary<string, ISymbolOrderBook>
|
||||||
{
|
{
|
||||||
{ "Binance", new BinanceSpotSymbolOrderBook("ETHBTC") },
|
{ "Binance", binanceFactory.CreateSpot("ETHBTC") },
|
||||||
{ "Bitfinex", new BitfinexSymbolOrderBook("tETHBTC") },
|
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
|
||||||
{ "Bittrex", new BittrexSymbolOrderBook("ETH-BTC") },
|
{ "Bittrex", bittrexFactory.Create("ETH-BTC") },
|
||||||
{ "Bybit", new BybitSpotSymbolOrderBook("ETHBTC") },
|
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
|
||||||
{ "CoinEx", new CoinExSpotSymbolOrderBook("ETHBTC") },
|
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
||||||
{ "FTX", new FTXSymbolOrderBook("ETH/BTC") },
|
{ "Huobi", huobiFactory.CreateSpot("ethbtc") },
|
||||||
{ "Huobi", new HuobiSpotSymbolOrderBook("ethbtc") },
|
{ "Kraken", krakenFactory.CreateSpot("ETH/XBT") },
|
||||||
{ "Kraken", new KrakenSpotSymbolOrderBook("ETH/XBT") },
|
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
|
||||||
{ "Kucoin", new KucoinSpotSymbolOrderBook("ETH-BTC") },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
|
await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
|
||||||
@@ -70,7 +74,7 @@
|
|||||||
{
|
{
|
||||||
_timer.Stop();
|
_timer.Stop();
|
||||||
_timer.Dispose();
|
_timer.Dispose();
|
||||||
foreach (var book in _books)
|
foreach (var book in _books.Where(b => b.Value.Status != CryptoExchange.Net.Objects.OrderBookStatus.Disconnected))
|
||||||
// It's not necessary to wait for this
|
// It's not necessary to wait for this
|
||||||
_ = book.Value.StopAsync();
|
_ = book.Value.StopAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
@page "/SpotClient"
|
@page "/SpotClient"
|
||||||
@inject IBinanceClient binanceClient
|
@inject IBinanceRestClient binanceClient
|
||||||
@inject IBitfinexClient bitfinexClient
|
@inject IBitfinexRestClient bitfinexClient
|
||||||
@inject IBittrexClient bittrexClient
|
@inject IBittrexRestClient bittrexClient
|
||||||
@inject IBybitClient bybitClient
|
@inject IBybitRestClient bybitClient
|
||||||
@inject ICoinExClient coinexClient
|
@inject ICoinExRestClient coinexClient
|
||||||
@inject IFTXClient ftxClient
|
@inject IHuobiRestClient huobiClient
|
||||||
@inject IHuobiClient huobiClient
|
@inject IKrakenRestClient krakenClient
|
||||||
@inject IKrakenClient krakenClient
|
@inject IKucoinRestClient kucoinClient
|
||||||
@inject IKucoinClient kucoinClient
|
|
||||||
@using Binance.Net.Clients.SpotApi
|
@using Binance.Net.Clients.SpotApi
|
||||||
@using Bitfinex.Net.Clients.SpotApi
|
@using Bitfinex.Net.Clients.SpotApi
|
||||||
@using Bittrex.Net.Clients.SpotApi
|
@using Bittrex.Net.Clients.SpotApi
|
||||||
@@ -15,7 +14,6 @@
|
|||||||
@using CoinEx.Net.Clients.SpotApi
|
@using CoinEx.Net.Clients.SpotApi
|
||||||
@using CryptoExchange.Net.Interfaces
|
@using CryptoExchange.Net.Interfaces
|
||||||
@using CryptoExchange.Net.Interfaces.CommonClients
|
@using CryptoExchange.Net.Interfaces.CommonClients
|
||||||
@using FTX.Net.Clients.TradeApi
|
|
||||||
@using Huobi.Net.Clients.SpotApi
|
@using Huobi.Net.Clients.SpotApi
|
||||||
@using Kraken.Net.Clients.SpotApi
|
@using Kraken.Net.Clients.SpotApi
|
||||||
@using Kucoin.Net.Clients.SpotApi
|
@using Kucoin.Net.Clients.SpotApi
|
||||||
@@ -37,9 +35,8 @@
|
|||||||
binanceClient.SpotApi.CommonSpotClient,
|
binanceClient.SpotApi.CommonSpotClient,
|
||||||
bitfinexClient.SpotApi.CommonSpotClient,
|
bitfinexClient.SpotApi.CommonSpotClient,
|
||||||
bittrexClient.SpotApi.CommonSpotClient,
|
bittrexClient.SpotApi.CommonSpotClient,
|
||||||
bybitClient.SpotApi.CommonSpotClient,
|
bybitClient.SpotApiV1.CommonSpotClient,
|
||||||
coinexClient.SpotApi.CommonSpotClient,
|
coinexClient.SpotApi.CommonSpotClient,
|
||||||
ftxClient.TradeApi.CommonSpotClient,
|
|
||||||
huobiClient.SpotApi.CommonSpotClient,
|
huobiClient.SpotApi.CommonSpotClient,
|
||||||
krakenClient.SpotApi.CommonSpotClient,
|
krakenClient.SpotApi.CommonSpotClient,
|
||||||
kucoinClient.SpotApi.CommonSpotClient
|
kucoinClient.SpotApi.CommonSpotClient
|
||||||
|
|||||||
@@ -2,15 +2,11 @@ using System.Collections.Generic;
|
|||||||
using Binance.Net;
|
using Binance.Net;
|
||||||
using Binance.Net.Clients;
|
using Binance.Net.Clients;
|
||||||
using Binance.Net.Interfaces.Clients;
|
using Binance.Net.Interfaces.Clients;
|
||||||
using Binance.Net.Objects;
|
|
||||||
using Bitfinex.Net;
|
using Bitfinex.Net;
|
||||||
using Bittrex.Net;
|
using Bittrex.Net;
|
||||||
using Bybit.Net;
|
using Bybit.Net;
|
||||||
using CoinEx.Net;
|
using CoinEx.Net;
|
||||||
using CoinEx.Net.Clients;
|
|
||||||
using CoinEx.Net.Interfaces.Clients;
|
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using FTX.Net;
|
|
||||||
using Huobi.Net;
|
using Huobi.Net;
|
||||||
using Kraken.Net;
|
using Kraken.Net;
|
||||||
using Kucoin.Net;
|
using Kucoin.Net;
|
||||||
@@ -43,35 +39,18 @@ namespace BlazorClient
|
|||||||
services.AddServerSideBlazor();
|
services.AddServerSideBlazor();
|
||||||
|
|
||||||
// Register the clients, options can be provided in the callback parameter
|
// Register the clients, options can be provided in the callback parameter
|
||||||
services.AddBinance((restClientOptions, socketClientOptions) => {
|
services.AddBinance(restOptions =>
|
||||||
restClientOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
|
||||||
restClientOptions.LogLevel = LogLevel.Trace;
|
|
||||||
|
|
||||||
// Point the logging to use the ILogger configuration, which uses Serilog here
|
|
||||||
restClientOptions.LogWriters = new List<ILogger> { _loggerFactory.CreateLogger<IBinanceClient>() };
|
|
||||||
|
|
||||||
socketClientOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
|
||||||
});
|
|
||||||
|
|
||||||
BinanceClient.SetDefaultOptions(new BinanceClientOptions
|
|
||||||
{
|
{
|
||||||
ApiCredentials = new ApiCredentials("KEY", "SECRET"),
|
restOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||||
LogLevel = LogLevel.Trace
|
}, socketOptions =>
|
||||||
});
|
|
||||||
|
|
||||||
BinanceSocketClient.SetDefaultOptions(new BinanceSocketClientOptions
|
|
||||||
{
|
{
|
||||||
ApiCredentials = new ApiCredentials("KEY", "SECRET"),
|
socketOptions.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddTransient<IBinanceClient, BinanceClient>();
|
|
||||||
services.AddScoped<IBinanceSocketClient, BinanceSocketClient>();
|
|
||||||
|
|
||||||
services.AddBitfinex();
|
services.AddBitfinex();
|
||||||
services.AddBittrex();
|
services.AddBittrex();
|
||||||
services.AddBybit();
|
services.AddBybit();
|
||||||
services.AddCoinEx();
|
services.AddCoinEx();
|
||||||
services.AddFTX();
|
|
||||||
services.AddHuobi();
|
services.AddHuobi();
|
||||||
services.AddKraken();
|
services.AddKraken();
|
||||||
services.AddKucoin();
|
services.AddKucoin();
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
@using Bittrex.Net.Interfaces.Clients;
|
@using Bittrex.Net.Interfaces.Clients;
|
||||||
@using Bybit.Net.Interfaces.Clients;
|
@using Bybit.Net.Interfaces.Clients;
|
||||||
@using CoinEx.Net.Interfaces.Clients;
|
@using CoinEx.Net.Interfaces.Clients;
|
||||||
@using FTX.Net.Interfaces.Clients;
|
|
||||||
@using Huobi.Net.Interfaces.Clients;
|
@using Huobi.Net.Interfaces.Clients;
|
||||||
@using Kraken.Net.Interfaces.Clients;
|
@using Kraken.Net.Interfaces.Clients;
|
||||||
@using Kucoin.Net.Interfaces.Clients;
|
@using Kucoin.Net.Interfaces.Clients;
|
||||||
@@ -6,15 +6,14 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="8.0.6" />
|
<PackageReference Include="Binance.Net" Version="9.0.1" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="5.0.3" />
|
<PackageReference Include="Bitfinex.Net" Version="6.0.0" />
|
||||||
<PackageReference Include="Bittrex.Net" Version="7.0.4" />
|
<PackageReference Include="Bittrex.Net" Version="8.0.0" />
|
||||||
<PackageReference Include="Bybit.Net" Version="0.0.4" />
|
<PackageReference Include="Bybit.Net" Version="3.0.0" />
|
||||||
<PackageReference Include="CoinEx.Net" Version="5.0.3" />
|
<PackageReference Include="CoinEx.Net" Version="6.0.0" />
|
||||||
<PackageReference Include="FTX.Net" Version="1.0.4" />
|
<PackageReference Include="Huobi.Net" Version="5.0.0" />
|
||||||
<PackageReference Include="Huobi.Net" Version="4.0.4" />
|
<PackageReference Include="KrakenExchange.Net" Version="4.0.0" />
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="3.0.3" />
|
<PackageReference Include="Kucoin.Net" Version="5.0.0" />
|
||||||
<PackageReference Include="Kucoin.Net" Version="4.0.3" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -17,21 +17,21 @@ namespace ConsoleClient.Exchanges
|
|||||||
|
|
||||||
public async Task<WebCallResult> CancelOrder(string symbol, string id)
|
public async Task<WebCallResult> CancelOrder(string symbol, string id)
|
||||||
{
|
{
|
||||||
using var client = new BinanceClient();
|
using var client = new BinanceRestClient();
|
||||||
var result = await client.SpotApi.Trading.CancelOrderAsync(symbol, long.Parse(id));
|
var result = await client.SpotApi.Trading.CancelOrderAsync(symbol, long.Parse(id));
|
||||||
return result.AsDataless();
|
return result.AsDataless();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Dictionary<string, decimal>> GetBalances()
|
public async Task<Dictionary<string, decimal>> GetBalances()
|
||||||
{
|
{
|
||||||
using var client = new BinanceClient();
|
using var client = new BinanceRestClient();
|
||||||
var result = await client.SpotApi.Account.GetAccountInfoAsync();
|
var result = await client.SpotApi.Account.GetAccountInfoAsync();
|
||||||
return result.Data.Balances.ToDictionary(b => b.Asset, b => b.Total);
|
return result.Data.Balances.ToDictionary(b => b.Asset, b => b.Total);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
|
public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
|
||||||
{
|
{
|
||||||
using var client = new BinanceClient();
|
using var client = new BinanceRestClient();
|
||||||
var result = await client.SpotApi.Trading.GetOpenOrdersAsync();
|
var result = await client.SpotApi.Trading.GetOpenOrdersAsync();
|
||||||
// Should check result success status here
|
// Should check result success status here
|
||||||
return result.Data.Select(o => new OpenOrder
|
return result.Data.Select(o => new OpenOrder
|
||||||
@@ -49,7 +49,7 @@ namespace ConsoleClient.Exchanges
|
|||||||
|
|
||||||
public async Task<decimal> GetPrice(string symbol)
|
public async Task<decimal> GetPrice(string symbol)
|
||||||
{
|
{
|
||||||
using var client = new BinanceClient();
|
using var client = new BinanceRestClient();
|
||||||
var result = await client.SpotApi.ExchangeData.GetPriceAsync(symbol);
|
var result = await client.SpotApi.ExchangeData.GetPriceAsync(symbol);
|
||||||
// Should check result success status here
|
// Should check result success status here
|
||||||
return result.Data.Price;
|
return result.Data.Price;
|
||||||
@@ -57,7 +57,7 @@ namespace ConsoleClient.Exchanges
|
|||||||
|
|
||||||
public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price)
|
public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price)
|
||||||
{
|
{
|
||||||
using var client = new BinanceClient();
|
using var client = new BinanceRestClient();
|
||||||
var result = await client.SpotApi.Trading.PlaceOrderAsync(
|
var result = await client.SpotApi.Trading.PlaceOrderAsync(
|
||||||
symbol,
|
symbol,
|
||||||
side.ToLower() == "buy" ? Binance.Net.Enums.OrderSide.Buy: Binance.Net.Enums.OrderSide.Sell,
|
side.ToLower() == "buy" ? Binance.Net.Enums.OrderSide.Buy: Binance.Net.Enums.OrderSide.Sell,
|
||||||
@@ -70,7 +70,7 @@ namespace ConsoleClient.Exchanges
|
|||||||
|
|
||||||
public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler)
|
public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler)
|
||||||
{
|
{
|
||||||
var sub = await _socketClient.SpotStreams.SubscribeToMiniTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice));
|
var sub = await _socketClient.SpotApi.ExchangeData.SubscribeToMiniTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice));
|
||||||
return sub.Data;
|
return sub.Data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using Bybit.Net.Clients;
|
||||||
|
using Bybit.Net.Interfaces.Clients;
|
||||||
|
using ConsoleClient.Models;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Sockets;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ConsoleClient.Exchanges
|
||||||
|
{
|
||||||
|
internal class BybitExchange : IExchange
|
||||||
|
{
|
||||||
|
private IBybitSocketClient _socketClient = new BybitSocketClient();
|
||||||
|
|
||||||
|
public async Task<WebCallResult> CancelOrder(string symbol, string id)
|
||||||
|
{
|
||||||
|
using var client = new BybitRestClient();
|
||||||
|
var result = await client.V5Api.Trading.CancelOrderAsync(Bybit.Net.Enums.Category.Spot, symbol, id);
|
||||||
|
return result.AsDataless();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, decimal>> GetBalances()
|
||||||
|
{
|
||||||
|
using var client = new BybitRestClient();
|
||||||
|
var result = await client.V5Api.Account.GetBalancesAsync(Bybit.Net.Enums.AccountType.Spot);
|
||||||
|
return result.Data.List.First().Assets.ToDictionary(d => d.Asset, d => d.WalletBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
|
||||||
|
{
|
||||||
|
using var client = new BybitRestClient();
|
||||||
|
var order = await client.V5Api.Trading.GetOrdersAsync(Bybit.Net.Enums.Category.Spot);
|
||||||
|
return order.Data.List.Select(o => new OpenOrder
|
||||||
|
{
|
||||||
|
Symbol = o.Symbol,
|
||||||
|
OrderSide = o.Side.ToString(),
|
||||||
|
OrderStatus = o.Status.ToString(),
|
||||||
|
OrderTime = o.CreateTime,
|
||||||
|
OrderType = o.OrderType.ToString(),
|
||||||
|
Price = o.Price ?? 0,
|
||||||
|
Quantity = o.Quantity,
|
||||||
|
QuantityFilled = o.QuantityFilled ?? 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<decimal> GetPrice(string symbol)
|
||||||
|
{
|
||||||
|
using var client = new BybitRestClient();
|
||||||
|
var result = await client.V5Api.ExchangeData.GetSpotTickersAsync(symbol);
|
||||||
|
return result.Data.List.First().LastPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price)
|
||||||
|
{
|
||||||
|
using var client = new BybitRestClient();
|
||||||
|
var result = await client.V5Api.Trading.PlaceOrderAsync(
|
||||||
|
Bybit.Net.Enums.Category.Spot,
|
||||||
|
symbol,
|
||||||
|
side.ToLower() == "buy" ? Bybit.Net.Enums.OrderSide.Buy : Bybit.Net.Enums.OrderSide.Sell,
|
||||||
|
type == "market" ? Bybit.Net.Enums.NewOrderType.Market : Bybit.Net.Enums.NewOrderType.Limit,
|
||||||
|
quantity,
|
||||||
|
price: price);
|
||||||
|
return result.As(result.Data?.OrderId.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler)
|
||||||
|
{
|
||||||
|
var sub = await _socketClient.V5SpotApi.SubscribeToTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice));
|
||||||
|
return sub.Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
using ConsoleClient.Models;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using FTX.Net.Clients;
|
|
||||||
using FTX.Net.Interfaces.Clients;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ConsoleClient.Exchanges
|
|
||||||
{
|
|
||||||
internal class FTXExchange : IExchange
|
|
||||||
{
|
|
||||||
private IFTXSocketClient _socketClient = new FTXSocketClient();
|
|
||||||
|
|
||||||
public async Task<WebCallResult> CancelOrder(string symbol, string id)
|
|
||||||
{
|
|
||||||
using var client = new FTXClient();
|
|
||||||
var result = await client.TradeApi.Trading.CancelOrderAsync(long.Parse(id));
|
|
||||||
return result.AsDataless();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Dictionary<string, decimal>> GetBalances()
|
|
||||||
{
|
|
||||||
using var client = new FTXClient();
|
|
||||||
var result = await client.TradeApi.Account.GetBalancesAsync();
|
|
||||||
return result.Data.ToDictionary(d => d.Asset, d => d.Total);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
|
|
||||||
{
|
|
||||||
using var client = new FTXClient();
|
|
||||||
var order = await client.TradeApi.Trading.GetOpenOrdersAsync();
|
|
||||||
return order.Data.Select(o => new OpenOrder
|
|
||||||
{
|
|
||||||
Symbol = o.Symbol,
|
|
||||||
OrderSide = o.Side.ToString(),
|
|
||||||
OrderStatus = o.Status.ToString(),
|
|
||||||
OrderTime = o.CreateTime,
|
|
||||||
OrderType = o.Type.ToString(),
|
|
||||||
Price = o.Price ?? 0,
|
|
||||||
Quantity = o.Quantity,
|
|
||||||
QuantityFilled = o.QuantityFilled ?? 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<decimal> GetPrice(string symbol)
|
|
||||||
{
|
|
||||||
using var client = new FTXClient();
|
|
||||||
var result = await client.TradeApi.ExchangeData.GetSymbolAsync(symbol);
|
|
||||||
return result.Data.LastPrice ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<WebCallResult<string>> PlaceOrder(string symbol, string side, string type, decimal quantity, decimal? price)
|
|
||||||
{
|
|
||||||
using var client = new FTXClient();
|
|
||||||
var result = await client.TradeApi.Trading.PlaceOrderAsync(
|
|
||||||
symbol,
|
|
||||||
side.ToLower() == "buy" ? FTX.Net.Enums.OrderSide.Buy : FTX.Net.Enums.OrderSide.Sell,
|
|
||||||
type == "market" ? FTX.Net.Enums.OrderType.Market : FTX.Net.Enums.OrderType.Limit,
|
|
||||||
quantity,
|
|
||||||
price: price);
|
|
||||||
return result.As(result.Data?.Id.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<UpdateSubscription> SubscribePrice(string symbol, Action<decimal> handler)
|
|
||||||
{
|
|
||||||
var sub = await _socketClient.Streams.SubscribeToTickerUpdatesAsync(symbol, data => handler(data.Data.LastPrice ?? 0));
|
|
||||||
return sub.Data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,12 +5,10 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Binance.Net.Clients;
|
using Binance.Net.Clients;
|
||||||
using Binance.Net.Objects;
|
using Binance.Net.Objects;
|
||||||
|
using Bybit.Net.Clients;
|
||||||
using ConsoleClient.Exchanges;
|
using ConsoleClient.Exchanges;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using FTX.Net.Clients;
|
|
||||||
using FTX.Net.Objects;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace ConsoleClient
|
namespace ConsoleClient
|
||||||
{
|
{
|
||||||
@@ -19,20 +17,18 @@ namespace ConsoleClient
|
|||||||
static Dictionary<string, IExchange> _exchanges = new Dictionary<string, IExchange>
|
static Dictionary<string, IExchange> _exchanges = new Dictionary<string, IExchange>
|
||||||
{
|
{
|
||||||
{ "Binance", new BinanceExchange() },
|
{ "Binance", new BinanceExchange() },
|
||||||
{ "FTX", new FTXExchange() }
|
{ "Bybit", new BybitExchange() }
|
||||||
};
|
};
|
||||||
|
|
||||||
static async Task Main(string[] args)
|
static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
BinanceClient.SetDefaultOptions(new BinanceClientOptions
|
BinanceRestClient.SetDefaultOptions(options =>
|
||||||
{
|
{
|
||||||
LogLevel = LogLevel.Trace,
|
options.ApiCredentials = new ApiCredentials("APIKEY", "APISECRET");
|
||||||
ApiCredentials = new ApiCredentials("APIKEY", "APISECRET")
|
|
||||||
});
|
});
|
||||||
FTXClient.SetDefaultOptions(new FTXClientOptions
|
BybitRestClient.SetDefaultOptions(options =>
|
||||||
{
|
{
|
||||||
LogLevel = LogLevel.Trace,
|
options.ApiCredentials = new ApiCredentials("APIKEY", "APISECRET");
|
||||||
ApiCredentials = new ApiCredentials("APIKEY", "APISECRET")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
|
|||||||
@@ -25,14 +25,27 @@ Use one of the following following referral links to signup to a new exchange to
|
|||||||
### Donate
|
### Donate
|
||||||
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
|
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
|
||||||
|
|
||||||
**Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS
|
**Btc**: bc1qz0jv0my7fc60rxeupr23e75x95qmlq6489n8gh
|
||||||
**Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2
|
**Eth**: 0x8E21C4d955975cB645589745ac0c46ECA8FAE504
|
||||||
**Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs
|
|
||||||
|
|
||||||
### Sponsor
|
### Sponsor
|
||||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 6.1.0 - 24 Aug 2023
|
||||||
|
* Added support for ratelimiting on socket connections
|
||||||
|
* Added rest ratelimit handling and parsing
|
||||||
|
* Added ServerRatelimitError error
|
||||||
|
|
||||||
|
* Version 6.0.3 - 23 Jul 2023
|
||||||
|
* Fixed Proxy not getting applied in rest clients when not using DI
|
||||||
|
|
||||||
|
* Version 6.0.2 - 05 Jul 2023
|
||||||
|
* Added properties generic dictionary to SocketConnection
|
||||||
|
|
||||||
|
* Version 6.0.1 - 29 Jun 2023
|
||||||
|
* Added LogLevel optional parameter to TraceLoggerProvider
|
||||||
|
|
||||||
* Version 6.0.0 - 25 Jun 2023
|
* Version 6.0.0 - 25 Jun 2023
|
||||||
* Updated ApiCredentials to support RSA signing as well as the default Hmac signature
|
* Updated ApiCredentials to support RSA signing as well as the default Hmac signature
|
||||||
* Removed custom logging implementation in favor of using `Microsoft.Extensions.Logging` ILogger directly
|
* Removed custom logging implementation in favor of using `Microsoft.Extensions.Logging` ILogger directly
|
||||||
|
|||||||
+19
-19
@@ -11,15 +11,15 @@ Each implementation generally provides two different clients, which will be the
|
|||||||
The rest client gives access to the Rest endpoint of the API. Rest endpoints are accessed by sending an HTTP request and receiving a response. The client is split in different sub-clients, which are named API Clients. These API clients are then again split in different topics. Typically a Rest client will look like this:
|
The rest client gives access to the Rest endpoint of the API. Rest endpoints are accessed by sending an HTTP request and receiving a response. The client is split in different sub-clients, which are named API Clients. These API clients are then again split in different topics. Typically a Rest client will look like this:
|
||||||
|
|
||||||
- [ExchangeName]RestClient
|
- [ExchangeName]RestClient
|
||||||
- SpotApi
|
- SpotApi
|
||||||
- Account
|
- Account
|
||||||
- ExchangeData
|
- ExchangeData
|
||||||
- Trading
|
- Trading
|
||||||
- FuturesApi
|
- FuturesApi
|
||||||
- Account
|
- Account
|
||||||
- ExchangeData
|
- ExchangeData
|
||||||
- Trading
|
- Trading
|
||||||
|
|
||||||
This rest client has 2 different API clients, the `SpotApi` and the `FuturesApi`, each offering their own set of endpoints.
|
This rest client has 2 different API clients, the `SpotApi` and the `FuturesApi`, each offering their own set of endpoints.
|
||||||
|
|
||||||
*Requesting ticker info on the spot API*
|
*Requesting ticker info on the spot API*
|
||||||
@@ -64,8 +64,8 @@ When processing the result of a call it should always be checked for success. No
|
|||||||
var callResult = await kucoinClient.SpotApi.ExchangeData.GetTickersAsync();
|
var callResult = await kucoinClient.SpotApi.ExchangeData.GetTickersAsync();
|
||||||
if(!callResult.Success)
|
if(!callResult.Success)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Request failed: " + callResult.Error);
|
Console.WriteLine("Request failed: " + callResult.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine("Result: " + callResult.Data);
|
Console.WriteLine("Result: " + callResult.Data);
|
||||||
@@ -78,8 +78,8 @@ Just like the Rest client is divided in Rest Api clients, the Socket client is d
|
|||||||
```csharp
|
```csharp
|
||||||
|
|
||||||
- KucoinSocketClient
|
- KucoinSocketClient
|
||||||
- SpotStreams
|
- SpotStreams
|
||||||
- FuturesStreams
|
- FuturesStreams
|
||||||
|
|
||||||
```
|
```
|
||||||
*Subscribing to updates for all tickers on the Spot Api*
|
*Subscribing to updates for all tickers on the Spot Api*
|
||||||
@@ -95,7 +95,7 @@ await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandle
|
|||||||
|
|
||||||
private static void DataHandler(DataEvent<KucoinStreamTick> updateData)
|
private static void DataHandler(DataEvent<KucoinStreamTick> updateData)
|
||||||
{
|
{
|
||||||
// Process updateData
|
// Process updateData
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ private static void DataHandler(DataEvent<KucoinStreamTick> updateData)
|
|||||||
```csharp
|
```csharp
|
||||||
await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(updateData =>
|
await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(updateData =>
|
||||||
{
|
{
|
||||||
// Process updateData
|
// Process updateData
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -122,16 +122,16 @@ Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. T
|
|||||||
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
|
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
|
||||||
if(!subscriptionResult.Success)
|
if(!subscriptionResult.Success)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Failed to connect: " + subscriptionResult.Error);
|
Console.WriteLine("Failed to connect: " + subscriptionResult.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
subscriptionResult.Data.ConnectionLost += () =>
|
subscriptionResult.Data.ConnectionLost += () =>
|
||||||
{
|
{
|
||||||
Console.WriteLine("Connection lost");
|
Console.WriteLine("Connection lost");
|
||||||
};
|
};
|
||||||
subscriptionResult.Data.ConnectionRestored += (time) =>
|
subscriptionResult.Data.ConnectionRestored += (time) =>
|
||||||
{
|
{
|
||||||
Console.WriteLine("Connection restored");
|
Console.WriteLine("Connection restored");
|
||||||
};
|
};
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+3
-3
@@ -28,7 +28,7 @@ private void SomeMethod()
|
|||||||
{
|
{
|
||||||
var socketClient = new BinanceSocketClient();
|
var socketClient = new BinanceSocketClient();
|
||||||
socketClient.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => {
|
socketClient.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => {
|
||||||
// Handle data
|
// Handle data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -41,7 +41,7 @@ private BinanceSocketClient _socketClient = new BinanceSocketClient();
|
|||||||
private void SomeMethod()
|
private void SomeMethod()
|
||||||
{
|
{
|
||||||
_socketClient.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => {
|
_socketClient.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => {
|
||||||
// Handle data
|
// Handle data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ Yes, generally these are all supported and can be configured by setting the Envi
|
|||||||
```csharp
|
```csharp
|
||||||
var client = new BinanceRestClient(options =>
|
var client = new BinanceRestClient(options =>
|
||||||
{
|
{
|
||||||
options.Environment = BinanceEnvironment.Testnet;
|
options.Environment = BinanceEnvironment.Testnet;
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -10,7 +10,7 @@ The library offers extensive logging, which depends on the dotnet `Microsoft.Ext
|
|||||||
```csharp
|
```csharp
|
||||||
IServiceCollection services = new ServiceCollection();
|
IServiceCollection services = new ServiceCollection();
|
||||||
services
|
services
|
||||||
.AddBinance()
|
.AddBinance()
|
||||||
.AddLogging(options =>
|
.AddLogging(options =>
|
||||||
{
|
{
|
||||||
options.SetMinimumLevel(LogLevel.Trace);
|
options.SetMinimumLevel(LogLevel.Trace);
|
||||||
@@ -66,8 +66,8 @@ IServiceCollection serviceCollection = new ServiceCollection();
|
|||||||
serviceCollection.AddBinance();
|
serviceCollection.AddBinance();
|
||||||
serviceCollection.AddLogging(options =>
|
serviceCollection.AddLogging(options =>
|
||||||
{
|
{
|
||||||
options.SetMinimumLevel(LogLevel.Trace);
|
options.SetMinimumLevel(LogLevel.Trace);
|
||||||
options.AddConsole();
|
options.AddConsole();
|
||||||
}).BuildServiceProvider();
|
}).BuildServiceProvider();
|
||||||
|
|
||||||
var client = serviceCollection.GetRequiredService<IBinanceRestClient>();
|
var client = serviceCollection.GetRequiredService<IBinanceRestClient>();
|
||||||
@@ -91,7 +91,7 @@ By default the `OriginalData` property in the `WebCallResult`/`DataEvent` object
|
|||||||
```csharp
|
```csharp
|
||||||
var client = new BinanceClient(options =>
|
var client = new BinanceClient(options =>
|
||||||
{
|
{
|
||||||
options.OutputOriginalData = true
|
options.OutputOriginalData = true
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ var originallyReceivedData = tickerResult.OriginalData;
|
|||||||
|
|
||||||
// Socket update
|
// Socket update
|
||||||
await client.SpotStreams.SubscribeToAllTickerUpdatesAsync(update => {
|
await client.SpotStreams.SubscribeToAllTickerUpdatesAsync(update => {
|
||||||
var originallyRecievedData = update.OriginalData;
|
var originallyRecievedData = update.OriginalData;
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ The HttpClient will now be received by the DI container instead of having to pas
|
|||||||
```csharp
|
```csharp
|
||||||
var client = new BinanceClient(new BinanceClientOptions(){
|
var client = new BinanceClient(new BinanceClientOptions(){
|
||||||
OutputOriginalData = true,
|
OutputOriginalData = true,
|
||||||
SpotApiOptions = new RestApiOptions {
|
SpotApiOptions = new RestApiOptions {
|
||||||
BaseAddress = BinanceApiAddresses.TestNet.RestClientAddress
|
BaseAddress = BinanceApiAddresses.TestNet.RestClientAddress
|
||||||
}
|
}
|
||||||
// Other options
|
// Other options
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
@@ -38,7 +38,7 @@ var client = new BinanceClient(new BinanceClientOptions(){
|
|||||||
```csharp
|
```csharp
|
||||||
var client = new BinanceClient(options => {
|
var client = new BinanceClient(options => {
|
||||||
options.OutputOriginalData = true;
|
options.OutputOriginalData = true;
|
||||||
options.Environment = BinanceEnvironment.Testnet;
|
options.Environment = BinanceEnvironment.Testnet;
|
||||||
// Other options
|
// Other options
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
@@ -53,10 +53,10 @@ With the change in options providing the DI extension methods for the IServiceCo
|
|||||||
```csharp
|
```csharp
|
||||||
builder.Services.AddKucoin((restOpts, socketOpts) =>
|
builder.Services.AddKucoin((restOpts, socketOpts) =>
|
||||||
{
|
{
|
||||||
restOpts.LogLevel = LogLevel.Debug;
|
restOpts.LogLevel = LogLevel.Debug;
|
||||||
restOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
|
restOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
|
||||||
socketOpts.LogLevel = LogLevel.Debug;
|
socketOpts.LogLevel = LogLevel.Debug;
|
||||||
socketOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
|
socketOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
|
||||||
}, ServiceLifetime.Singleton);
|
}, ServiceLifetime.Singleton);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+12
-8
@@ -12,8 +12,10 @@ Each implementation can be configured using client options. There are 2 ways to
|
|||||||
|
|
||||||
BinanceClient.SetDefaultOptions(options =>
|
BinanceClient.SetDefaultOptions(options =>
|
||||||
{
|
{
|
||||||
options.OutputOriginalData = true;
|
options.OutputOriginalData = true;
|
||||||
options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||||
|
// Override the api credentials for the Spot API
|
||||||
|
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
|
||||||
});
|
});
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -23,8 +25,10 @@ BinanceClient.SetDefaultOptions(options =>
|
|||||||
|
|
||||||
var client = new BinanceClient(options =>
|
var client = new BinanceClient(options =>
|
||||||
{
|
{
|
||||||
options.OutputOriginalData = true;
|
options.OutputOriginalData = true;
|
||||||
options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
|
||||||
|
// Override the api credentials for the Spot API
|
||||||
|
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
|
||||||
});
|
});
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -34,12 +38,12 @@ When calling `SetDefaultOptions` each client created after that will use the opt
|
|||||||
|
|
||||||
BinanceClient.SetDefaultOptions(options =>
|
BinanceClient.SetDefaultOptions(options =>
|
||||||
{
|
{
|
||||||
options.OutputOriginalData = true;
|
options.OutputOriginalData = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
var client = new BinanceClient(options =>
|
var client = new BinanceClient(options =>
|
||||||
{
|
{
|
||||||
options.OutputOriginalData = false;
|
options.OutputOriginalData = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -54,8 +58,8 @@ The options are divided in two categories. The basic options, which will apply t
|
|||||||
|
|
||||||
var client = new BinanceRestClient(options =>
|
var client = new BinanceRestClient(options =>
|
||||||
{
|
{
|
||||||
options.ApiCredentials = new ApiCredentials("GENERAL-KEY", "GENERAL-SECRET"),
|
options.ApiCredentials = new ApiCredentials("GENERAL-KEY", "GENERAL-SECRET"),
|
||||||
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
|
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
|
||||||
});
|
});
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+5
-5
@@ -18,15 +18,15 @@ book.OnStatusChange += (oldState, newState) => Console.WriteLine($"State changed
|
|||||||
var startResult = await book.StartAsync();
|
var startResult = await book.StartAsync();
|
||||||
if (!startResult.Success)
|
if (!startResult.Success)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Failed to start order book: " + startResult.Error);
|
Console.WriteLine("Failed to start order book: " + startResult.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
while(true)
|
while(true)
|
||||||
{
|
{
|
||||||
Console.Clear();
|
Console.Clear();
|
||||||
Console.WriteLine(book.ToString(3);
|
Console.WriteLine(book.ToString(3);
|
||||||
await Task.Delay(500);
|
await Task.Delay(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+8
-8
@@ -13,12 +13,12 @@ A rate limiter can be configured in the options like so:
|
|||||||
```csharp
|
```csharp
|
||||||
new ClientOptions
|
new ClientOptions
|
||||||
{
|
{
|
||||||
RateLimitingBehaviour = RateLimitingBehaviour.Wait,
|
RateLimitingBehaviour = RateLimitingBehaviour.Wait,
|
||||||
RateLimiters = new List<IRateLimiter>
|
RateLimiters = new List<IRateLimiter>
|
||||||
{
|
{
|
||||||
new RateLimiter()
|
new RateLimiter()
|
||||||
.AddTotalRateLimit(50, TimeSpan.FromSeconds(10))
|
.AddTotalRateLimit(50, TimeSpan.FromSeconds(10))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@ This will add a rate limiter for 50 requests per 10 seconds.
|
|||||||
A rate limiter can have multiple limits:
|
A rate limiter can have multiple limits:
|
||||||
```csharp
|
```csharp
|
||||||
new RateLimiter()
|
new RateLimiter()
|
||||||
.AddTotalRateLimit(50, TimeSpan.FromSeconds(10))
|
.AddTotalRateLimit(50, TimeSpan.FromSeconds(10))
|
||||||
.AddEndpointLimit("/api/order", 10, TimeSpan.FromSeconds(2))
|
.AddEndpointLimit("/api/order", 10, TimeSpan.FromSeconds(2))
|
||||||
```
|
```
|
||||||
This adds another limit of 10 requests per 2 seconds in addition to the 50 requests per 10 seconds limit.
|
This adds another limit of 10 requests per 2 seconds in addition to the 50 requests per 10 seconds limit.
|
||||||
These are the available rate limit configurations:
|
These are the available rate limit configurations:
|
||||||
|
|||||||
+1
-1
@@ -20,6 +20,7 @@ These will always be on the latest CryptoExchange.Net version and the latest ver
|
|||||||
|<a href="https://github.com/JKorf/Huobi.Net"><img src="https://github.com/JKorf/Huobi.Net/blob/master/Huobi.Net/Icon/icon.png?raw=true"></a>|Huobi|https://jkorf.github.io/Huobi.Net/|
|
|<a href="https://github.com/JKorf/Huobi.Net"><img src="https://github.com/JKorf/Huobi.Net/blob/master/Huobi.Net/Icon/icon.png?raw=true"></a>|Huobi|https://jkorf.github.io/Huobi.Net/|
|
||||||
|<a href="https://github.com/JKorf/Kraken.Net"><img src="https://github.com/JKorf/Kraken.Net/blob/master/Kraken.Net/Icon/icon.png?raw=true"></a>|Kraken|https://jkorf.github.io/Kraken.Net/|
|
|<a href="https://github.com/JKorf/Kraken.Net"><img src="https://github.com/JKorf/Kraken.Net/blob/master/Kraken.Net/Icon/icon.png?raw=true"></a>|Kraken|https://jkorf.github.io/Kraken.Net/|
|
||||||
|<a href="https://github.com/JKorf/Kucoin.Net"><img src="https://github.com/JKorf/Kucoin.Net/blob/master/Kucoin.Net/Icon/icon.png?raw=true"></a>|Kucoin|https://jkorf.github.io/Kucoin.Net/|
|
|<a href="https://github.com/JKorf/Kucoin.Net"><img src="https://github.com/JKorf/Kucoin.Net/blob/master/Kucoin.Net/Icon/icon.png?raw=true"></a>|Kucoin|https://jkorf.github.io/Kucoin.Net/|
|
||||||
|
|<a href="https://github.com/JKorf/OKX.Net"><img src="https://raw.githubusercontent.com/JKorf/OKX.Net/358d31f58d8ee51fc234bff1940878a8d0ce5676/Okex.Net/Icon/icon.png"></a>|OKX|https://jkorf.github.io/OKX.Net/|
|
||||||
|
|
||||||
**Implementations by third parties**
|
**Implementations by third parties**
|
||||||
These might not be compatible with other libraries, make sure to check the CryptoExchange.Net version.
|
These might not be compatible with other libraries, make sure to check the CryptoExchange.Net version.
|
||||||
@@ -31,7 +32,6 @@ These might not be compatible with other libraries, make sure to check the Crypt
|
|||||||
|<a href="https://github.com/ridicoulous/Bitmex.Net"><img src="https://github.com/ridicoulous/Bitmex.Net/blob/master/Bitmex.Net/Icon/icon.png?raw=true"></a>|Bitmex|
|
|<a href="https://github.com/ridicoulous/Bitmex.Net"><img src="https://github.com/ridicoulous/Bitmex.Net/blob/master/Bitmex.Net/Icon/icon.png?raw=true"></a>|Bitmex|
|
||||||
|<a href="https://github.com/intelligences/HitBTC.Net"><img src="https://github.com/intelligences/HitBTC.Net/blob/master/src/HitBTC.Net/Icon/icon.png?raw=true"></a>|HitBTC|
|
|<a href="https://github.com/intelligences/HitBTC.Net"><img src="https://github.com/intelligences/HitBTC.Net/blob/master/src/HitBTC.Net/Icon/icon.png?raw=true"></a>|HitBTC|
|
||||||
|<a href="https://github.com/EricGarnier/LiveCoin.Net"><img src="https://github.com/EricGarnier/LiveCoin.Net/blob/master/LiveCoin.Net/Icon/icon.png?raw=true"></a>|LiveCoin|
|
|<a href="https://github.com/EricGarnier/LiveCoin.Net"><img src="https://github.com/EricGarnier/LiveCoin.Net/blob/master/LiveCoin.Net/Icon/icon.png?raw=true"></a>|LiveCoin|
|
||||||
|<a href="https://github.com/burakoner/OKEx.Net"><img src="https://github.com/burakoner/OKEx.Net/blob/master/Okex.Net/Icon/icon.png?raw=true"></a>|OKEx|
|
|
||||||
|<a href="https://github.com/burakoner/Chiliz.Net"><img src="https://github.com/burakoner/Chiliz.Net/blob/master/Chiliz.Net/Icon/icon.png?raw=true"></a>|Chiliz|
|
|<a href="https://github.com/burakoner/Chiliz.Net"><img src="https://github.com/burakoner/Chiliz.Net/blob/master/Chiliz.Net/Icon/icon.png?raw=true"></a>|Chiliz|
|
||||||
|<a href="https://github.com/burakoner/BtcTurk.Net"><img src="https://github.com/burakoner/BtcTurk.Net/blob/master/BtcTurk.Net/Icon/icon.png?raw=true"></a>|BtcTurk|
|
|<a href="https://github.com/burakoner/BtcTurk.Net"><img src="https://github.com/burakoner/BtcTurk.Net/blob/master/BtcTurk.Net/Icon/icon.png?raw=true"></a>|BtcTurk|
|
||||||
|<a href="https://github.com/burakoner/Thodex.Net"><img src="https://github.com/burakoner/Thodex.Net/blob/master/Thodex.Net/Icon/icon.png?raw=true"></a>|Thodex|
|
|<a href="https://github.com/burakoner/Thodex.Net"><img src="https://github.com/burakoner/Thodex.Net/blob/master/Thodex.Net/Icon/icon.png?raw=true"></a>|Thodex|
|
||||||
|
|||||||
Reference in New Issue
Block a user