mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-13 09:23:04 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e40f2a15b6 | |||
| b94085a27a | |||
| 5e083811df | |||
| 7dcf5cd6ea | |||
| 1471a4733f | |||
| f39d9f7cfb | |||
| 9fab8faa45 | |||
| 226f175343 | |||
| 813bd9f5a1 | |||
| c8d2b4f09d | |||
| 6560b82a3e | |||
| e151af8f37 | |||
| bdf7a07c6f | |||
| a8ffe90bf2 | |||
| 3372b9eb44 | |||
| df25221960 | |||
| 7c67a014f5 |
@@ -304,55 +304,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleSubscriptionComplete(bool success, object? response)
|
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
|
||||||
{
|
if (!subscribeResult)
|
||||||
if (!success)
|
return new CallResult<UpdateSubscription>(subscribeResult.Error!);
|
||||||
return;
|
|
||||||
|
|
||||||
subscription.HandleSubQueryResponse(socketConnection, response);
|
|
||||||
subscription.Status = SubscriptionStatus.Subscribed;
|
|
||||||
if (ct != default)
|
|
||||||
{
|
|
||||||
subscription.CancellationTokenRegistration = ct.Register(async () =>
|
|
||||||
{
|
|
||||||
_logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id);
|
|
||||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
|
||||||
}, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subscription.Status = SubscriptionStatus.Subscribing;
|
|
||||||
var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
|
|
||||||
if (subQuery != null)
|
|
||||||
{
|
|
||||||
subQuery.OnComplete = () => HandleSubscriptionComplete(subQuery.Result?.Success ?? false, subQuery.Response);
|
|
||||||
|
|
||||||
// Send the request and wait for answer
|
|
||||||
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, ct).ConfigureAwait(false);
|
|
||||||
if (!subResult)
|
|
||||||
{
|
|
||||||
var isTimeout = subResult.Error is CancellationRequestedError;
|
|
||||||
if (isTimeout && subscription.Status == SubscriptionStatus.Subscribed)
|
|
||||||
{
|
|
||||||
// No response received, but the subscription did receive updates. We'll assume success
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
|
||||||
// If this was a server process error we still might need to send an unsubscribe to prevent messages coming in later
|
|
||||||
subscription.Status = SubscriptionStatus.Pending;
|
|
||||||
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
|
||||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subQuery.ExpectsResponse)
|
|
||||||
HandleSubscriptionComplete(true, null);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HandleSubscriptionComplete(true, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
||||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (!_unknownValuesWarned.Contains(stringValue))
|
if (!_unknownValuesWarned.Contains(stringValue))
|
||||||
{
|
{
|
||||||
_unknownValuesWarned.Add(stringValue!);
|
_unknownValuesWarned.Add(stringValue!);
|
||||||
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: {string.Join(", ", _mappingToEnum!.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]. If you think {stringValue} should added please open an issue on the Github repo");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +246,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
// If no explicit mapping is found try to parse string
|
// If no explicit mapping is found try to parse string
|
||||||
result = (T)Enum.Parse(objectType, value, true);
|
result = (T)Enum.Parse(objectType, value, true);
|
||||||
|
if (!Enum.IsDefined(objectType, result))
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||||
<PackageVersion>10.5.1</PackageVersion>
|
<PackageVersion>10.6.0</PackageVersion>
|
||||||
<AssemblyVersion>10.5.1</AssemblyVersion>
|
<AssemblyVersion>10.6.0</AssemblyVersion>
|
||||||
<FileVersion>10.5.1</FileVersion>
|
<FileVersion>10.6.0</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
|
|||||||
@@ -448,6 +448,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
DateTime? serverDataTime = null,
|
DateTime? serverDataTime = null,
|
||||||
DateTime? localDataTime = null)
|
DateTime? localDataTime = null)
|
||||||
{
|
{
|
||||||
|
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
|
||||||
|
throw new InvalidOperationException("Trying to set snapshot while book is not working");
|
||||||
|
|
||||||
_processQueue.Enqueue(
|
_processQueue.Enqueue(
|
||||||
new OrderBookSnapshot
|
new OrderBookSnapshot
|
||||||
{
|
{
|
||||||
@@ -475,6 +478,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
DateTime? serverDataTime = null,
|
DateTime? serverDataTime = null,
|
||||||
DateTime? localDataTime = null)
|
DateTime? localDataTime = null)
|
||||||
{
|
{
|
||||||
|
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
|
||||||
|
throw new InvalidOperationException("Trying to update order book while book is not working");
|
||||||
|
|
||||||
_processQueue.Enqueue(
|
_processQueue.Enqueue(
|
||||||
new OrderBookUpdate
|
new OrderBookUpdate
|
||||||
{
|
{
|
||||||
@@ -505,6 +511,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
DateTime? serverDataTime = null,
|
DateTime? serverDataTime = null,
|
||||||
DateTime? localDataTime = null)
|
DateTime? localDataTime = null)
|
||||||
{
|
{
|
||||||
|
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
|
||||||
|
throw new InvalidOperationException("Trying to update order book while book is not working");
|
||||||
|
|
||||||
_processQueue.Enqueue(
|
_processQueue.Enqueue(
|
||||||
new OrderBookUpdate
|
new OrderBookUpdate
|
||||||
{
|
{
|
||||||
@@ -531,6 +540,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
DateTime? serverDataTime = null,
|
DateTime? serverDataTime = null,
|
||||||
DateTime? localDataTime = null)
|
DateTime? localDataTime = null)
|
||||||
{
|
{
|
||||||
|
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
|
||||||
|
throw new InvalidOperationException("Trying to update order book while book is not working");
|
||||||
|
|
||||||
var highest = Math.Max(bids.Any() ? bids.Max(b => b.Sequence) : 0, asks.Any() ? asks.Max(a => a.Sequence) : 0);
|
var highest = Math.Max(bids.Any() ? bids.Max(b => b.Sequence) : 0, asks.Any() ? asks.Max(a => a.Sequence) : 0);
|
||||||
var lowest = Math.Min(bids.Any() ? bids.Min(b => b.Sequence) : long.MaxValue, asks.Any() ? asks.Min(a => a.Sequence) : long.MaxValue);
|
var lowest = Math.Min(bids.Any() ? bids.Min(b => b.Sequence) : long.MaxValue, asks.Any() ? asks.Min(a => a.Sequence) : long.MaxValue);
|
||||||
|
|
||||||
@@ -554,6 +566,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// <param name="sequenceNumber">The sequence number of the message if it's a separate message with separate number</param>
|
/// <param name="sequenceNumber">The sequence number of the message if it's a separate message with separate number</param>
|
||||||
protected void AddChecksum(int checksum, long? sequenceNumber = null)
|
protected void AddChecksum(int checksum, long? sequenceNumber = null)
|
||||||
{
|
{
|
||||||
|
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
|
||||||
|
throw new InvalidOperationException("Trying to add checksum while book is not working");
|
||||||
|
|
||||||
_processQueue.Enqueue(new OrderBookChecksum() { Checksum = checksum, SequenceNumber = sequenceNumber });
|
_processQueue.Enqueue(new OrderBookChecksum() { Checksum = checksum, SequenceNumber = sequenceNumber });
|
||||||
_queueEvent.Set();
|
_queueEvent.Set();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Data.Common;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
namespace CryptoExchange.Net.SharedApis
|
||||||
@@ -99,6 +100,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
if (val == null)
|
if (val == null)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
|
if (val.Value is T typeVal)
|
||||||
|
return typeVal;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Type t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
Type t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
|
|||||||
@@ -673,13 +673,16 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
|
|
||||||
if (!processed)
|
if (!processed)
|
||||||
{
|
{
|
||||||
lock (_listenersLock)
|
if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data))
|
||||||
{
|
{
|
||||||
_logger.ReceivedMessageNotMatchedToAnyListener(
|
lock (_listenersLock)
|
||||||
SocketId,
|
{
|
||||||
typeIdentifier,
|
_logger.ReceivedMessageNotMatchedToAnyListener(
|
||||||
topicFilter!,
|
SocketId,
|
||||||
string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Where(x => x.TypeIdentifier == typeIdentifier).Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]")))));
|
typeIdentifier,
|
||||||
|
topicFilter!,
|
||||||
|
string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Where(x => x.TypeIdentifier == typeIdentifier).Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]")))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1082,40 +1085,8 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
var taskList = new List<Task<CallResult>>();
|
var taskList = new List<Task<CallResult>>();
|
||||||
foreach (var subscription in subList)
|
foreach (var subscription in subList)
|
||||||
{
|
{
|
||||||
subscription.ConnectionInvocations = 0;
|
var subscribeTask = TrySubscribeAsync(subscription, false, default);
|
||||||
if (!subscription.Active)
|
taskList.Add(subscribeTask);
|
||||||
// Can be closed during resubscribing
|
|
||||||
continue;
|
|
||||||
|
|
||||||
subscription.Status = SubscriptionStatus.Subscribing;
|
|
||||||
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
|
||||||
if (!result)
|
|
||||||
{
|
|
||||||
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
|
||||||
subscription.Status = SubscriptionStatus.Pending;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
var subQuery = subscription.CreateSubscriptionQuery(this);
|
|
||||||
if (subQuery == null)
|
|
||||||
{
|
|
||||||
subscription.Status = SubscriptionStatus.Subscribed;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
subQuery.OnComplete = () =>
|
|
||||||
{
|
|
||||||
subscription.Status = subQuery.Result!.Success ? SubscriptionStatus.Subscribed : SubscriptionStatus.Pending;
|
|
||||||
subscription.HandleSubQueryResponse(this, subQuery.Response);
|
|
||||||
};
|
|
||||||
|
|
||||||
taskList.Add(SendAndWaitQueryAsync(subQuery));
|
|
||||||
|
|
||||||
if (!subQuery.ExpectsResponse)
|
|
||||||
{
|
|
||||||
// If there won't be an answer we can immediately set this
|
|
||||||
subscription.Status = SubscriptionStatus.Subscribed;
|
|
||||||
subscription.HandleSubQueryResponse(this, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(taskList).ConfigureAwait(false);
|
await Task.WhenAll(taskList).ConfigureAwait(false);
|
||||||
@@ -1132,6 +1103,67 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
return CallResult.SuccessResult;
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to subscribe a new subscription by sending the subscribe query and wait for the result as needed
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscription">The subscription</param>
|
||||||
|
/// <param name="newSubscription">Whether this is a new subscription, or an existing subscription (resubscribing on reconnected socket)</param>
|
||||||
|
/// <param name="subCancelToken">Cancellation token</param>
|
||||||
|
protected internal async Task<CallResult> TrySubscribeAsync(Subscription subscription, bool newSubscription, CancellationToken subCancelToken)
|
||||||
|
{
|
||||||
|
subscription.ConnectionInvocations = 0;
|
||||||
|
|
||||||
|
if (!newSubscription)
|
||||||
|
{
|
||||||
|
if (!subscription.Active)
|
||||||
|
// Can be closed during resubscribing
|
||||||
|
return CallResult.SuccessResult;
|
||||||
|
|
||||||
|
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
||||||
|
subscription.Status = SubscriptionStatus.Pending;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.Status = SubscriptionStatus.Subscribing;
|
||||||
|
var subQuery = subscription.CreateSubscriptionQuery(this);
|
||||||
|
if (subQuery == null)
|
||||||
|
{
|
||||||
|
// No sub query, so successful
|
||||||
|
subscription.Status = SubscriptionStatus.Subscribed;
|
||||||
|
return CallResult.SuccessResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
subQuery.OnComplete = () =>
|
||||||
|
{
|
||||||
|
subscription.Status = subQuery.Result!.Success ? SubscriptionStatus.Subscribed : SubscriptionStatus.Pending;
|
||||||
|
subscription.HandleSubQueryResponse(this, subQuery.Response);
|
||||||
|
if (newSubscription && subQuery.Result.Success && subCancelToken != default)
|
||||||
|
{
|
||||||
|
subscription.CancellationTokenRegistration = subCancelToken.Register(async () =>
|
||||||
|
{
|
||||||
|
_logger.CancellationTokenSetClosingSubscription(SocketId, subscription.Id);
|
||||||
|
await CloseAsync(subscription).ConfigureAwait(false);
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var subQueryResult = await SendAndWaitQueryAsync(subQuery).ConfigureAwait(false);
|
||||||
|
if (!subQueryResult)
|
||||||
|
{
|
||||||
|
_logger.FailedToSubscribe(SocketId, subQueryResult.Error?.ToString());
|
||||||
|
// If this was a server process error or timeout we still send an unsubscribe to prevent messages coming in later
|
||||||
|
if (newSubscription)
|
||||||
|
await CloseAsync(subscription).ConfigureAwait(false);
|
||||||
|
return new CallResult<UpdateSubscription>(subQueryResult.Error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return subQueryResult;
|
||||||
|
}
|
||||||
|
|
||||||
internal async Task UnsubscribeAsync(Subscription subscription)
|
internal async Task UnsubscribeAsync(Subscription subscription)
|
||||||
{
|
{
|
||||||
var unsubscribeRequest = subscription.CreateUnsubscriptionQuery(this);
|
var unsubscribeRequest = subscription.CreateUnsubscriptionQuery(this);
|
||||||
|
|||||||
@@ -174,6 +174,14 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
ConnectionInvocations++;
|
ConnectionInvocations++;
|
||||||
TotalInvocations++;
|
TotalInvocations++;
|
||||||
|
if (SubscriptionQuery != null && !SubscriptionQuery.Completed && SubscriptionQuery.TimeoutBehavior == TimeoutBehavior.Succeed)
|
||||||
|
{
|
||||||
|
// The subscription query is one where it is successful if there is no error returned
|
||||||
|
// Since we've received a data update for the subscription we can assume the subscribe query was successful
|
||||||
|
// Call timeout to complete
|
||||||
|
SubscriptionQuery.Timeout();
|
||||||
|
}
|
||||||
|
|
||||||
return route.Handle(connection, receiveTime, originalData, data);
|
return route.Handle(connection, receiveTime, originalData, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,8 +127,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Completed = true;
|
|
||||||
Result = CallResult.SuccessResult;
|
Result = CallResult.SuccessResult;
|
||||||
|
Completed = true;
|
||||||
_event.Set();
|
_event.Set();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,12 +216,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (Completed)
|
if (Completed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Completed = true;
|
|
||||||
if (TimeoutBehavior == TimeoutBehavior.Fail)
|
if (TimeoutBehavior == TimeoutBehavior.Fail)
|
||||||
Result = new CallResult<THandlerResponse>(new TimeoutError());
|
Result = new CallResult<THandlerResponse>(new TimeoutError());
|
||||||
else
|
else
|
||||||
Result = new CallResult<THandlerResponse>(default, null, default);
|
Result = new CallResult<THandlerResponse>(default, null, default);
|
||||||
|
|
||||||
|
Completed = true;
|
||||||
_event.Set();
|
_event.Set();
|
||||||
OnComplete?.Invoke();
|
OnComplete?.Invoke();
|
||||||
}
|
}
|
||||||
@@ -234,6 +234,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
Result = new CallResult<THandlerResponse>(error);
|
Result = new CallResult<THandlerResponse>(error);
|
||||||
Completed = true;
|
Completed = true;
|
||||||
|
|
||||||
_event.Set();
|
_event.Set();
|
||||||
OnComplete?.Invoke();
|
OnComplete?.Invoke();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,6 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
bool Connected { get; }
|
bool Connected { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Currently tracked symbols. Data for these symbols will be requested when polling.
|
|
||||||
/// Websocket updates will be available for all symbols regardless.
|
|
||||||
/// When new data is received for a symbol which is not yet being tracked it will be added to this list and polled in the future unless the `OnlyTrackProvidedSymbols` option is set in the configuration.
|
|
||||||
/// </summary>
|
|
||||||
IEnumerable<SharedSymbol> TrackedSymbols { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// On connection status change. Might trigger multiple times with the same status depending on the underlying subscriptions.
|
/// On connection status change. Might trigger multiple times with the same status depending on the underlying subscriptions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
||||||
@@ -26,6 +27,13 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Exchange { get; }
|
public string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Currently tracked symbols. Data for these symbols will be requested when polling.
|
||||||
|
/// Websocket updates will be available for all symbols regardless.
|
||||||
|
/// When new data is received for a symbol which is not yet being tracked it will be added to this list and polled in the future unless the `OnlyTrackProvidedSymbols` option is set in the configuration.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<SharedSymbol> TrackedSymbols { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Balances tracker
|
/// Balances tracker
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -57,5 +65,18 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task StopAsync();
|
Task StopAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add symbols to the list of symbols for which data is being tracked
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbols">Symbols to add</param>
|
||||||
|
void AddTrackedSymbolsAsync(IEnumerable<SharedSymbol> symbols);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a symbol from the list of symbols for which data is being tracked.
|
||||||
|
/// Note that the symbol will be added again if new data for that symbol is received, unless the OnlyTrackProvidedSymbols option has been set to true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">Symbol to remove</param>
|
||||||
|
void RemoveTrackedSymbolAsync(SharedSymbol symbol);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
||||||
@@ -26,6 +27,13 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Exchange { get; }
|
public string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Currently tracked symbols. Data for these symbols will be requested when polling.
|
||||||
|
/// Websocket updates will be available for all symbols regardless.
|
||||||
|
/// When new data is received for a symbol which is not yet being tracked it will be added to this list and polled in the future unless the `OnlyTrackProvidedSymbols` option is set in the configuration.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<SharedSymbol> TrackedSymbols { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Balances tracker
|
/// Balances tracker
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -53,5 +61,18 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task StopAsync();
|
Task StopAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add symbols to the list of symbols for which data is being tracked
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbols">Symbols to add</param>
|
||||||
|
void AddTrackedSymbolsAsync(IEnumerable<SharedSymbol> symbols);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a symbol from the list of symbols for which data is being tracked.
|
||||||
|
/// Note that the symbol will be added again if new data for that symbol is received, unless the OnlyTrackProvidedSymbols option has been set to true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">Symbol to remove</param>
|
||||||
|
void RemoveTrackedSymbolAsync(SharedSymbol symbol);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,12 +22,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public BalanceTracker(
|
public BalanceTracker(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
IBalanceRestClient restClient,
|
IBalanceRestClient restClient,
|
||||||
IBalanceSocketClient? socketClient,
|
IBalanceSocketClient? socketClient,
|
||||||
SharedAccountType accountType,
|
SharedAccountType accountType,
|
||||||
TrackerItemConfig config,
|
TrackerItemConfig config,
|
||||||
ExchangeParameters? exchangeParameters = null
|
ExchangeParameters? exchangeParameters = null
|
||||||
) : base(logger, UserDataType.Balances, restClient.Exchange, config, false, null)
|
) : base(logger, symbolTracker, UserDataType.Balances, restClient.Exchange, config)
|
||||||
{
|
{
|
||||||
if (_socketClient == null)
|
if (_socketClient == null)
|
||||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
private readonly IFuturesOrderSocketClient? _socketClient;
|
private readonly IFuturesOrderSocketClient? _socketClient;
|
||||||
private readonly ExchangeParameters? _exchangeParameters;
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
private readonly bool _requiresSymbolParameterOpenOrders;
|
private readonly bool _requiresSymbolParameterOpenOrders;
|
||||||
|
private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new();
|
||||||
|
|
||||||
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
||||||
|
|
||||||
@@ -27,13 +28,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public FuturesOrderTracker(
|
public FuturesOrderTracker(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
IFuturesOrderRestClient restClient,
|
IFuturesOrderRestClient restClient,
|
||||||
IFuturesOrderSocketClient? socketClient,
|
IFuturesOrderSocketClient? socketClient,
|
||||||
TrackerItemConfig config,
|
TrackerItemConfig config,
|
||||||
IEnumerable<SharedSymbol> symbols,
|
IEnumerable<SharedSymbol> symbols,
|
||||||
bool onlyTrackProvidedSymbols,
|
bool onlyTrackProvidedSymbols,
|
||||||
ExchangeParameters? exchangeParameters = null
|
ExchangeParameters? exchangeParameters = null
|
||||||
) : base(logger, UserDataType.Orders, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
) : base(logger, symbolTracker, UserDataType.Orders, restClient.Exchange, config)
|
||||||
{
|
{
|
||||||
if (_socketClient == null)
|
if (_socketClient == null)
|
||||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
@@ -45,6 +47,20 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
_requiresSymbolParameterOpenOrders = restClient.GetOpenFuturesOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol");
|
_requiresSymbolParameterOpenOrders = restClient.GetOpenFuturesOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void ClearDataForSymbol(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
foreach (var order in _store)
|
||||||
|
{
|
||||||
|
if (order.Value.SharedSymbol!.TradingMode == symbol.TradingMode
|
||||||
|
&& order.Value.SharedSymbol.BaseAsset == symbol.BaseAsset
|
||||||
|
&& order.Value.SharedSymbol.QuoteAsset == symbol.QuoteAsset
|
||||||
|
&& order.Value.SharedSymbol.DeliverTime == symbol.DeliverTime)
|
||||||
|
{
|
||||||
|
_store.TryRemove(order.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override bool Update(SharedFuturesOrder existingItem, SharedFuturesOrder updateItem)
|
protected override bool Update(SharedFuturesOrder existingItem, SharedFuturesOrder updateItem)
|
||||||
{
|
{
|
||||||
@@ -233,7 +249,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var symbol in _symbols.ToList())
|
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||||
{
|
{
|
||||||
var openOrdersResult = await _restClient.GetOpenFuturesOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var openOrdersResult = await _restClient.GetOpenFuturesOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!openOrdersResult.Success)
|
if (!openOrdersResult.Success)
|
||||||
@@ -251,11 +267,30 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var symbol in _symbols.ToList())
|
if (!_firstPollDone && anyError)
|
||||||
|
return anyError;
|
||||||
|
|
||||||
|
// Check all current open orders
|
||||||
|
// Keep track of the orders no longer returned in the open list
|
||||||
|
// Order should be set to canceled state when it's no longer returned in the open list
|
||||||
|
// but also is not returned in the closed list
|
||||||
|
foreach (var order in Values.Where(x => x.Status == SharedOrderStatus.Open))
|
||||||
{
|
{
|
||||||
var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
if (openOrders.Any(x => x.OrderId == order.OrderId))
|
||||||
var updatedPollTime = DateTime.UtcNow;
|
continue;
|
||||||
|
|
||||||
|
if (!_openOrderNotReturnedTimes.ContainsKey(order.OrderId))
|
||||||
|
_openOrderNotReturnedTimes[order.OrderId] = 0;
|
||||||
|
|
||||||
|
_openOrderNotReturnedTimes[order.OrderId] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
|
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||||
|
{
|
||||||
|
DateTime? fromTimeOrders = GetClosedOrdersRequestStartTime(symbol);
|
||||||
|
|
||||||
var closedOrdersResult = await _restClient.GetClosedFuturesOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var closedOrdersResult = await _restClient.GetClosedFuturesOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!closedOrdersResult.Success)
|
if (!closedOrdersResult.Success)
|
||||||
{
|
{
|
||||||
@@ -267,22 +302,26 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_lastDataTimeBeforeDisconnect = null;
|
|
||||||
_lastPollTime = updatedPollTime;
|
|
||||||
|
|
||||||
// Filter orders to only include where close time is after the start time
|
// Filter orders to only include where close time is after the start time
|
||||||
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
||||||
(x.UpdateTime != null && x.UpdateTime >= _startTime) // Updated after the tracker start time
|
(x.UpdateTime != null && x.UpdateTime >= _startTime) // Updated after the tracker start time
|
||||||
|| (x.CreateTime != null && x.CreateTime >= _startTime) // Created after the tracker start time
|
|| (x.CreateTime != null && x.CreateTime >= _startTime) // Created after the tracker start time
|
||||||
|| (x.CreateTime == null && x.UpdateTime == null) // Unknown time
|
|| (x.CreateTime == null && x.UpdateTime == null) // Unknown time
|
||||||
|
|| (Values.Any(e => e.OrderId == x.OrderId && x.Status == SharedOrderStatus.Open)) // Or we're currently tracking this open order
|
||||||
).ToArray();
|
).ToArray();
|
||||||
|
|
||||||
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
||||||
var openOrdersNotReturned = Values.Where(x =>
|
var openOrdersNotReturned = Values.Where(x =>
|
||||||
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset // Orders for the same symbol
|
// Orders for the same symbol
|
||||||
&& x.QuantityFilled?.IsZero == true // With no filled value
|
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset
|
||||||
&& !openOrders.Any(r => r.OrderId == x.OrderId) // Not returned in open orders
|
// With no filled value
|
||||||
&& !relevantOrders.Any(r => r.OrderId == x.OrderId) // Not return in closed orders
|
&& x.QuantityFilled?.IsZero == true
|
||||||
|
// Not returned in open orders
|
||||||
|
&& !openOrders.Any(r => r.OrderId == x.OrderId)
|
||||||
|
// Not returned in closed orders
|
||||||
|
&& !relevantOrders.Any(r => r.OrderId == x.OrderId)
|
||||||
|
// Open order has not been returned in the open list at least 2 times
|
||||||
|
&& (_openOrderNotReturnedTimes.TryGetValue(x.OrderId, out var notReturnedTimes) ? notReturnedTimes >= 2 : false)
|
||||||
).ToList();
|
).ToList();
|
||||||
|
|
||||||
var additionalUpdates = new List<SharedFuturesOrder>();
|
var additionalUpdates = new List<SharedFuturesOrder>();
|
||||||
@@ -300,7 +339,64 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!anyError)
|
||||||
|
{
|
||||||
|
_lastPollTime = updatedPollTime;
|
||||||
|
_lastDataTimeBeforeDisconnect = null;
|
||||||
|
}
|
||||||
|
|
||||||
return anyError;
|
return anyError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DateTime? GetClosedOrdersRequestStartTime(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
// Determine the timestamp from which we need to check order status
|
||||||
|
// Use the timestamp we last know the correct state of the data
|
||||||
|
DateTime? fromTime = null;
|
||||||
|
string? source = null;
|
||||||
|
|
||||||
|
// Use the last timestamp we we received data from the websocket as state should be correct at that time. 1 seconds buffer
|
||||||
|
if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value))
|
||||||
|
{
|
||||||
|
fromTime = _lastDataTimeBeforeDisconnect.Value.AddSeconds(-1);
|
||||||
|
source = "LastDataTimeBeforeDisconnect";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've previously polled use that timestamp to request data from
|
||||||
|
if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime))
|
||||||
|
{
|
||||||
|
fromTime = _lastPollTime;
|
||||||
|
source = "LastPollTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we known open orders with a create time before this time we need to use that timestamp to make sure that order is included in the response
|
||||||
|
var trackedOrdersMinOpenTime = Values
|
||||||
|
.Where(x => x.Status == SharedOrderStatus.Open && x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset)
|
||||||
|
.OrderBy(x => x.CreateTime)
|
||||||
|
.FirstOrDefault()?.CreateTime;
|
||||||
|
if (trackedOrdersMinOpenTime.HasValue && (fromTime == null || trackedOrdersMinOpenTime.Value < fromTime))
|
||||||
|
{
|
||||||
|
// Could be improved by only requesting the specific open orders if there are only a few that would be better than trying to request a long
|
||||||
|
// history if the open order is far back
|
||||||
|
fromTime = trackedOrdersMinOpenTime.Value.AddMilliseconds(-1);
|
||||||
|
source = "OpenOrder";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromTime == null)
|
||||||
|
{
|
||||||
|
fromTime = _startTime;
|
||||||
|
source = "StartTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(1))
|
||||||
|
{
|
||||||
|
// Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync
|
||||||
|
fromTime = DateTime.UtcNow.AddSeconds(-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogTrace("{DataType}.{Symbol} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}",
|
||||||
|
DataType, $"{symbol.BaseAsset}/{symbol.QuoteAsset}", source, fromTime);
|
||||||
|
return fromTime!.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public FuturesUserTradeTracker(
|
public FuturesUserTradeTracker(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
IFuturesOrderRestClient restClient,
|
IFuturesOrderRestClient restClient,
|
||||||
IUserTradeSocketClient? socketClient,
|
IUserTradeSocketClient? socketClient,
|
||||||
TrackerItemConfig config,
|
TrackerItemConfig config,
|
||||||
IEnumerable<SharedSymbol> symbols,
|
IEnumerable<SharedSymbol> symbols,
|
||||||
bool onlyTrackProvidedSymbols,
|
bool onlyTrackProvidedSymbols,
|
||||||
ExchangeParameters? exchangeParameters = null
|
ExchangeParameters? exchangeParameters = null
|
||||||
) : base(logger, UserDataType.Trades, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
) : base(logger, symbolTracker, UserDataType.Trades, restClient.Exchange, config)
|
||||||
{
|
{
|
||||||
if (_socketClient == null)
|
if (_socketClient == null)
|
||||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
@@ -42,6 +43,20 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
_exchangeParameters = exchangeParameters;
|
_exchangeParameters = exchangeParameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void ClearDataForSymbol(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
foreach (var order in _store)
|
||||||
|
{
|
||||||
|
if (order.Value.SharedSymbol!.TradingMode == symbol.TradingMode
|
||||||
|
&& order.Value.SharedSymbol.BaseAsset == symbol.BaseAsset
|
||||||
|
&& order.Value.SharedSymbol.QuoteAsset == symbol.QuoteAsset
|
||||||
|
&& order.Value.SharedSymbol.DeliverTime == symbol.DeliverTime)
|
||||||
|
{
|
||||||
|
_store.TryRemove(order.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override string GetKey(SharedUserTrade item) => item.Id;
|
protected override string GetKey(SharedUserTrade item) => item.Id;
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -55,10 +70,10 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
protected override async Task<bool> DoPollAsync()
|
protected override async Task<bool> DoPollAsync()
|
||||||
{
|
{
|
||||||
var anyError = false;
|
var anyError = false;
|
||||||
foreach (var symbol in _symbols)
|
var fromTimeTrades = GetTradesRequestStartTime();
|
||||||
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
|
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||||
{
|
{
|
||||||
var fromTimeTrades = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
|
||||||
var updatedPollTime = DateTime.UtcNow;
|
|
||||||
var tradesResult = await _restClient.GetFuturesUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var tradesResult = await _restClient.GetFuturesUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!tradesResult.Success)
|
if (!tradesResult.Success)
|
||||||
{
|
{
|
||||||
@@ -80,9 +95,53 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!anyError)
|
||||||
|
{
|
||||||
|
_lastDataTimeBeforeDisconnect = null;
|
||||||
|
_lastPollTime = updatedPollTime;
|
||||||
|
}
|
||||||
|
|
||||||
return anyError;
|
return anyError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DateTime? GetTradesRequestStartTime()
|
||||||
|
{
|
||||||
|
// Determine the timestamp from which we need to check order status
|
||||||
|
// Use the timestamp we last know the correct state of the data
|
||||||
|
DateTime? fromTime = null;
|
||||||
|
string? source = null;
|
||||||
|
|
||||||
|
// Use the last timestamp we we received data from the websocket as state should be correct at that time. 1 seconds buffer
|
||||||
|
if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value))
|
||||||
|
{
|
||||||
|
fromTime = _lastDataTimeBeforeDisconnect.Value.AddSeconds(-1);
|
||||||
|
source = "LastDataTimeBeforeDisconnect";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've previously polled use that timestamp to request data from
|
||||||
|
if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime))
|
||||||
|
{
|
||||||
|
fromTime = _lastPollTime;
|
||||||
|
source = "LastPollTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromTime == null)
|
||||||
|
{
|
||||||
|
fromTime = _startTime;
|
||||||
|
source = "StartTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (now - fromTime < TimeSpan.FromSeconds(1))
|
||||||
|
{
|
||||||
|
// Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync
|
||||||
|
fromTime = DateTime.UtcNow.AddSeconds(-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogTrace("{DataType} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}", DataType, source, fromTime);
|
||||||
|
return fromTime!.Value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public PositionTracker(
|
public PositionTracker(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
IFuturesOrderRestClient restClient,
|
IFuturesOrderRestClient restClient,
|
||||||
IPositionSocketClient? socketClient,
|
IPositionSocketClient? socketClient,
|
||||||
TrackerItemConfig config,
|
TrackerItemConfig config,
|
||||||
@@ -36,7 +37,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
bool onlyTrackProvidedSymbols,
|
bool onlyTrackProvidedSymbols,
|
||||||
bool websocketPositionUpdatesAreFullSnapshots,
|
bool websocketPositionUpdatesAreFullSnapshots,
|
||||||
ExchangeParameters? exchangeParameters = null
|
ExchangeParameters? exchangeParameters = null
|
||||||
) : base(logger, UserDataType.Positions, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
) : base(logger, symbolTracker, UserDataType.Positions, restClient.Exchange, config)
|
||||||
{
|
{
|
||||||
if (_socketClient == null)
|
if (_socketClient == null)
|
||||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
@@ -118,9 +119,9 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
{
|
{
|
||||||
toRemove ??= new List<SharedPosition>();
|
toRemove ??= new List<SharedPosition>();
|
||||||
toRemove.Add(item);
|
toRemove.Add(item);
|
||||||
|
_logger.LogTrace("Ignoring {DataType} update for {Key}, no SharedSymbol set", DataType, item.Symbol);
|
||||||
}
|
}
|
||||||
else if (_onlyTrackProvidedSymbols
|
else if (!_symbolTracker.ShouldProcess(symbolModel.SharedSymbol))
|
||||||
&& !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
|
|
||||||
{
|
{
|
||||||
toRemove ??= new List<SharedPosition>();
|
toRemove ??= new List<SharedPosition>();
|
||||||
toRemove.Add(item);
|
toRemove.Add(item);
|
||||||
@@ -131,8 +132,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
if (toRemove != null)
|
if (toRemove != null)
|
||||||
@event = @event.Except(toRemove).ToArray();
|
@event = @event.Except(toRemove).ToArray();
|
||||||
|
|
||||||
if (!_onlyTrackProvidedSymbols)
|
_symbolTracker.UpdateTrackedSymbols(@event.Where(x => x.PositionSize > 0).OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
||||||
UpdateSymbolsList(@event.Where(x => x.PositionSize > 0).OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
|
||||||
|
|
||||||
|
|
||||||
// Update local store
|
// Update local store
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
private readonly ISpotOrderSocketClient? _socketClient;
|
private readonly ISpotOrderSocketClient? _socketClient;
|
||||||
private readonly ExchangeParameters? _exchangeParameters;
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
private readonly bool _requiresSymbolParameterOpenOrders;
|
private readonly bool _requiresSymbolParameterOpenOrders;
|
||||||
|
private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new();
|
||||||
|
|
||||||
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
||||||
|
|
||||||
@@ -27,13 +28,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public SpotOrderTracker(
|
public SpotOrderTracker(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
ISpotOrderRestClient restClient,
|
ISpotOrderRestClient restClient,
|
||||||
ISpotOrderSocketClient? socketClient,
|
ISpotOrderSocketClient? socketClient,
|
||||||
TrackerItemConfig config,
|
TrackerItemConfig config,
|
||||||
IEnumerable<SharedSymbol> symbols,
|
IEnumerable<SharedSymbol> symbols,
|
||||||
bool onlyTrackProvidedSymbols,
|
bool onlyTrackProvidedSymbols,
|
||||||
ExchangeParameters? exchangeParameters = null
|
ExchangeParameters? exchangeParameters = null
|
||||||
) : base(logger, UserDataType.Orders, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
) : base(logger, symbolTracker, UserDataType.Orders, restClient.Exchange, config)
|
||||||
{
|
{
|
||||||
if (_socketClient == null)
|
if (_socketClient == null)
|
||||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
@@ -45,6 +47,19 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
_requiresSymbolParameterOpenOrders = restClient.GetOpenSpotOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol");
|
_requiresSymbolParameterOpenOrders = restClient.GetOpenSpotOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void ClearDataForSymbol(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
foreach(var order in _store)
|
||||||
|
{
|
||||||
|
if (order.Value.SharedSymbol!.TradingMode == symbol.TradingMode
|
||||||
|
&& order.Value.SharedSymbol.BaseAsset == symbol.BaseAsset
|
||||||
|
&& order.Value.SharedSymbol.QuoteAsset == symbol.QuoteAsset)
|
||||||
|
{
|
||||||
|
_store.TryRemove(order.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override bool Update(SharedSpotOrder existingItem, SharedSpotOrder updateItem)
|
protected override bool Update(SharedSpotOrder existingItem, SharedSpotOrder updateItem)
|
||||||
{
|
{
|
||||||
@@ -244,7 +259,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var symbol in _symbols.ToList())
|
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||||
{
|
{
|
||||||
var openOrdersResult = await _restClient.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var openOrdersResult = await _restClient.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!openOrdersResult.Success)
|
if (!openOrdersResult.Success)
|
||||||
@@ -266,10 +281,27 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
if (!_firstPollDone && anyError)
|
if (!_firstPollDone && anyError)
|
||||||
return anyError;
|
return anyError;
|
||||||
|
|
||||||
foreach (var symbol in _symbols.ToList())
|
// Check all current open orders
|
||||||
|
// Keep track of the orders no longer returned in the open list
|
||||||
|
// Order should be set to canceled state when it's no longer returned in the open list
|
||||||
|
// but also is not returned in the closed list
|
||||||
|
foreach (var order in Values.Where(x => x.Status == SharedOrderStatus.Open))
|
||||||
{
|
{
|
||||||
var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
if (openOrders.Any(x => x.OrderId == order.OrderId))
|
||||||
var updatedPollTime = DateTime.UtcNow;
|
continue;
|
||||||
|
|
||||||
|
if (!_openOrderNotReturnedTimes.ContainsKey(order.OrderId))
|
||||||
|
_openOrderNotReturnedTimes[order.OrderId] = 0;
|
||||||
|
|
||||||
|
_openOrderNotReturnedTimes[order.OrderId] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
|
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||||
|
{
|
||||||
|
DateTime? fromTimeOrders = GetClosedOrdersRequestStartTime(symbol);
|
||||||
|
|
||||||
|
|
||||||
var closedOrdersResult = await _restClient.GetClosedSpotOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var closedOrdersResult = await _restClient.GetClosedSpotOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!closedOrdersResult.Success)
|
if (!closedOrdersResult.Success)
|
||||||
{
|
{
|
||||||
@@ -281,22 +313,26 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_lastDataTimeBeforeDisconnect = null;
|
|
||||||
_lastPollTime = updatedPollTime;
|
|
||||||
|
|
||||||
// Filter orders to only include where close time is after the start time
|
// Filter orders to only include where close time is after the start time
|
||||||
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
||||||
(x.UpdateTime != null && x.UpdateTime >= _startTime) // Updated after the tracker start time
|
(x.UpdateTime != null && x.UpdateTime >= _startTime) // Updated after the tracker start time
|
||||||
|| (x.CreateTime != null && x.CreateTime >= _startTime) // Created after the tracker start time
|
|| (x.CreateTime != null && x.CreateTime >= _startTime) // Created after the tracker start time
|
||||||
|| (x.CreateTime == null && x.UpdateTime == null) // Unknown time
|
|| (x.CreateTime == null && x.UpdateTime == null) // Unknown time
|
||||||
|
|| (Values.Any(e => e.OrderId == x.OrderId && x.Status == SharedOrderStatus.Open)) // Or we're currently tracking this open order
|
||||||
).ToArray();
|
).ToArray();
|
||||||
|
|
||||||
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
||||||
var openOrdersNotReturned = Values.Where(x =>
|
var openOrdersNotReturned = Values.Where(x =>
|
||||||
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset // Orders for the same symbol
|
// Orders for the same symbol
|
||||||
&& x.QuantityFilled?.IsZero == true // With no filled value
|
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset
|
||||||
&& !openOrders.Any(r => r.OrderId == x.OrderId) // Not returned in open orders
|
// With no filled value
|
||||||
&& !relevantOrders.Any(r => r.OrderId == x.OrderId) // Not return in closed orders
|
&& x.QuantityFilled?.IsZero == true
|
||||||
|
// Not returned in open orders
|
||||||
|
&& !openOrders.Any(r => r.OrderId == x.OrderId)
|
||||||
|
// Not returned in closed orders
|
||||||
|
&& !relevantOrders.Any(r => r.OrderId == x.OrderId)
|
||||||
|
// Open order has not been returned in the open list at least 2 times
|
||||||
|
&& (_openOrderNotReturnedTimes.TryGetValue(x.OrderId, out var notReturnedTimes) ? notReturnedTimes >= 2 : false)
|
||||||
).ToList();
|
).ToList();
|
||||||
|
|
||||||
var additionalUpdates = new List<SharedSpotOrder>();
|
var additionalUpdates = new List<SharedSpotOrder>();
|
||||||
@@ -314,7 +350,64 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!anyError)
|
||||||
|
{
|
||||||
|
_lastDataTimeBeforeDisconnect = null;
|
||||||
|
_lastPollTime = updatedPollTime;
|
||||||
|
}
|
||||||
|
|
||||||
return anyError;
|
return anyError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DateTime? GetClosedOrdersRequestStartTime(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
// Determine the timestamp from which we need to check order status
|
||||||
|
// Use the timestamp we last know the correct state of the data
|
||||||
|
DateTime? fromTime = null;
|
||||||
|
string? source = null;
|
||||||
|
|
||||||
|
// Use the last timestamp we we received data from the websocket as state should be correct at that time. 1 seconds buffer
|
||||||
|
if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value))
|
||||||
|
{
|
||||||
|
fromTime = _lastDataTimeBeforeDisconnect.Value.AddSeconds(-1);
|
||||||
|
source = "LastDataTimeBeforeDisconnect";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've previously polled use that timestamp to request data from
|
||||||
|
if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime))
|
||||||
|
{
|
||||||
|
fromTime = _lastPollTime;
|
||||||
|
source = "LastPollTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we known open orders with a create time before this time we need to use that timestamp to make sure that order is included in the response
|
||||||
|
var trackedOrdersMinOpenTime = Values
|
||||||
|
.Where(x => x.Status == SharedOrderStatus.Open && x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset)
|
||||||
|
.OrderBy(x => x.CreateTime)
|
||||||
|
.FirstOrDefault()?.CreateTime;
|
||||||
|
if (trackedOrdersMinOpenTime.HasValue && (fromTime == null || trackedOrdersMinOpenTime.Value < fromTime))
|
||||||
|
{
|
||||||
|
// Could be improved by only requesting the specific open orders if there are only a few that would be better than trying to request a long
|
||||||
|
// history if the open order is far back
|
||||||
|
fromTime = trackedOrdersMinOpenTime.Value.AddMilliseconds(-1);
|
||||||
|
source = "OpenOrder";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromTime == null)
|
||||||
|
{
|
||||||
|
fromTime = _startTime;
|
||||||
|
source = "StartTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(1))
|
||||||
|
{
|
||||||
|
// Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync
|
||||||
|
fromTime = DateTime.UtcNow.AddSeconds(-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogTrace("{DataType}.{Symbol} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}",
|
||||||
|
DataType, $"{symbol.BaseAsset}/{symbol.QuoteAsset}", source, fromTime);
|
||||||
|
return fromTime!.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public SpotUserTradeTracker(
|
public SpotUserTradeTracker(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
ISpotOrderRestClient restClient,
|
ISpotOrderRestClient restClient,
|
||||||
IUserTradeSocketClient? socketClient,
|
IUserTradeSocketClient? socketClient,
|
||||||
TrackerItemConfig config,
|
TrackerItemConfig config,
|
||||||
IEnumerable<SharedSymbol> symbols,
|
IEnumerable<SharedSymbol> symbols,
|
||||||
bool onlyTrackProvidedSymbols,
|
bool onlyTrackProvidedSymbols,
|
||||||
ExchangeParameters? exchangeParameters = null
|
ExchangeParameters? exchangeParameters = null
|
||||||
) : base(logger, UserDataType.Trades, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
) : base(logger, symbolTracker, UserDataType.Trades, restClient.Exchange, config)
|
||||||
{
|
{
|
||||||
if (_socketClient == null)
|
if (_socketClient == null)
|
||||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
@@ -42,6 +43,19 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
_exchangeParameters = exchangeParameters;
|
_exchangeParameters = exchangeParameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void ClearDataForSymbol(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
foreach (var trade in _store)
|
||||||
|
{
|
||||||
|
if (trade.Value.SharedSymbol!.TradingMode == symbol.TradingMode
|
||||||
|
&& trade.Value.SharedSymbol.BaseAsset == symbol.BaseAsset
|
||||||
|
&& trade.Value.SharedSymbol.QuoteAsset == symbol.QuoteAsset)
|
||||||
|
{
|
||||||
|
_store.TryRemove(trade.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override string GetKey(SharedUserTrade item) => item.Id;
|
protected override string GetKey(SharedUserTrade item) => item.Id;
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -55,10 +69,10 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
protected override async Task<bool> DoPollAsync()
|
protected override async Task<bool> DoPollAsync()
|
||||||
{
|
{
|
||||||
var anyError = false;
|
var anyError = false;
|
||||||
foreach (var symbol in _symbols)
|
var fromTimeTrades = GetTradesRequestStartTime();
|
||||||
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
|
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||||
{
|
{
|
||||||
var fromTimeTrades = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
|
||||||
var updatedPollTime = DateTime.UtcNow;
|
|
||||||
var tradesResult = await _restClient.GetSpotUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var tradesResult = await _restClient.GetSpotUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!tradesResult.Success)
|
if (!tradesResult.Success)
|
||||||
{
|
{
|
||||||
@@ -70,8 +84,6 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_lastDataTimeBeforeDisconnect = null;
|
|
||||||
_lastPollTime = updatedPollTime;
|
|
||||||
|
|
||||||
// Filter trades to only include where timestamp is after the start time OR it's part of an order we're tracking
|
// Filter trades to only include where timestamp is after the start time OR it's part of an order we're tracking
|
||||||
var relevantTrades = tradesResult.Data.Where(x => x.Timestamp >= _startTime || (GetTrackedOrderIds?.Invoke() ?? []).Any(o => o == x.OrderId)).ToArray();
|
var relevantTrades = tradesResult.Data.Where(x => x.Timestamp >= _startTime || (GetTrackedOrderIds?.Invoke() ?? []).Any(o => o == x.OrderId)).ToArray();
|
||||||
@@ -80,9 +92,52 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!anyError)
|
||||||
|
{
|
||||||
|
_lastDataTimeBeforeDisconnect = null;
|
||||||
|
_lastPollTime = updatedPollTime;
|
||||||
|
}
|
||||||
|
|
||||||
return anyError;
|
return anyError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DateTime? GetTradesRequestStartTime()
|
||||||
|
{
|
||||||
|
// Determine the timestamp from which we need to check order status
|
||||||
|
// Use the timestamp we last know the correct state of the data
|
||||||
|
DateTime? fromTime = null;
|
||||||
|
string? source = null;
|
||||||
|
|
||||||
|
// Use the last timestamp we we received data from the websocket as state should be correct at that time. 1 seconds buffer
|
||||||
|
if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value))
|
||||||
|
{
|
||||||
|
fromTime = _lastDataTimeBeforeDisconnect.Value.AddSeconds(-1);
|
||||||
|
source = "LastDataTimeBeforeDisconnect";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've previously polled use that timestamp to request data from
|
||||||
|
if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime))
|
||||||
|
{
|
||||||
|
fromTime = _lastPollTime;
|
||||||
|
source = "LastPollTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromTime == null)
|
||||||
|
{
|
||||||
|
fromTime = _startTime;
|
||||||
|
source = "StartTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(1))
|
||||||
|
{
|
||||||
|
// Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync
|
||||||
|
fromTime = DateTime.UtcNow.AddSeconds(-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogTrace("{DataType} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}", DataType, source, fromTime);
|
||||||
|
return fromTime!.Value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -203,21 +203,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected ConcurrentDictionary<string, T> _store = new ConcurrentDictionary<string, T>(StringComparer.InvariantCultureIgnoreCase);
|
protected ConcurrentDictionary<string, T> _store = new ConcurrentDictionary<string, T>(StringComparer.InvariantCultureIgnoreCase);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tracked symbols list
|
|
||||||
/// </summary>
|
|
||||||
protected readonly List<SharedSymbol> _symbols;
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol lock
|
|
||||||
/// </summary>
|
|
||||||
protected object _symbolLock = new object();
|
|
||||||
/// <summary>
|
|
||||||
/// Only track provided symbols setting
|
|
||||||
/// </summary>
|
|
||||||
protected bool _onlyTrackProvidedSymbols;
|
|
||||||
/// <summary>
|
|
||||||
/// Is SharedSymbol model
|
/// Is SharedSymbol model
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _isSymbolModel;
|
protected bool _isSymbolModel;
|
||||||
|
/// <summary>
|
||||||
|
/// Symbol tracker
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
protected readonly UserDataSymbolTracker _symbolTracker;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public T[] Values
|
public T[] Values
|
||||||
@@ -240,22 +233,23 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Func<UserDataUpdate<T[]>, Task>? OnUpdate;
|
public event Func<UserDataUpdate<T[]>, Task>? OnUpdate;
|
||||||
/// <inheritdoc />
|
|
||||||
public IEnumerable<SharedSymbol> TrackedSymbols => _symbols;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UserDataItemTracker(ILogger logger, UserDataType dataType, string exchange, TrackerItemConfig config, bool onlyTrackProvidedSymbols, IEnumerable<SharedSymbol>? symbols) : base(logger, dataType, exchange)
|
public UserDataItemTracker(
|
||||||
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
|
UserDataType dataType,
|
||||||
|
string exchange,
|
||||||
|
TrackerItemConfig config) : base(logger, dataType, exchange)
|
||||||
{
|
{
|
||||||
_onlyTrackProvidedSymbols = onlyTrackProvidedSymbols;
|
|
||||||
_symbols = symbols?.ToList() ?? [];
|
|
||||||
|
|
||||||
_pollIntervalDisconnected = config.PollIntervalDisconnected;
|
_pollIntervalDisconnected = config.PollIntervalDisconnected;
|
||||||
_pollIntervalConnected = config.PollIntervalConnected;
|
_pollIntervalConnected = config.PollIntervalConnected;
|
||||||
_pollAtStart = config.PollAtStart;
|
_pollAtStart = config.PollAtStart;
|
||||||
_retentionTime = config is TrackerTimedItemConfig timeConfig ? timeConfig.RetentionTime : TimeSpan.MaxValue;
|
_retentionTime = config is TrackerTimedItemConfig timeConfig ? timeConfig.RetentionTime : TimeSpan.MaxValue;
|
||||||
_isSymbolModel = typeof(T).IsSubclassOf(typeof(SharedSymbolModel));
|
_isSymbolModel = typeof(T).IsSubclassOf(typeof(SharedSymbolModel));
|
||||||
|
_symbolTracker = symbolTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -334,26 +328,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// Get the age of an item
|
/// Get the age of an item
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual TimeSpan GetAge(DateTime time, T item) => TimeSpan.Zero;
|
protected virtual TimeSpan GetAge(DateTime time, T item) => TimeSpan.Zero;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update the tracked symbol list with potential new symbols
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="symbols"></param>
|
|
||||||
protected void UpdateSymbolsList(IEnumerable<SharedSymbol> symbols)
|
|
||||||
{
|
|
||||||
lock (_symbolLock)
|
|
||||||
{
|
|
||||||
foreach (var symbol in symbols.Distinct())
|
|
||||||
{
|
|
||||||
if (!_symbols.Any(x => x.TradingMode == symbol.TradingMode && x.BaseAsset == symbol.BaseAsset && x.QuoteAsset == symbol.QuoteAsset))
|
|
||||||
{
|
|
||||||
_symbols.Add(symbol);
|
|
||||||
_logger.LogDebug("Adding {BaseAsset}/{QuoteAsset} to symbol tracking list", symbol.BaseAsset, symbol.QuoteAsset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle an update
|
/// Handle an update
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -372,9 +347,9 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
{
|
{
|
||||||
toRemove ??= new List<T>();
|
toRemove ??= new List<T>();
|
||||||
toRemove.Add(item);
|
toRemove.Add(item);
|
||||||
|
_logger.LogWarning("Ignoring {DataType} update for {Key}, no SharedSymbol set", DataType, GetKey(item));
|
||||||
}
|
}
|
||||||
else if (_onlyTrackProvidedSymbols
|
else if (!_symbolTracker.ShouldProcess(symbolModel.SharedSymbol))
|
||||||
&& !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
|
|
||||||
{
|
{
|
||||||
toRemove ??= new List<T>();
|
toRemove ??= new List<T>();
|
||||||
toRemove.Add(item);
|
toRemove.Add(item);
|
||||||
@@ -385,8 +360,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
if (toRemove != null)
|
if (toRemove != null)
|
||||||
@event = @event.Except(toRemove).ToArray();
|
@event = @event.Except(toRemove).ToArray();
|
||||||
|
|
||||||
if (!_onlyTrackProvidedSymbols)
|
_symbolTracker.UpdateTrackedSymbols(@event.OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
||||||
UpdateSymbolsList(@event.OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update local store
|
// Update local store
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tracker for symbols used in UserDataTracker
|
||||||
|
/// </summary>
|
||||||
|
public class UserDataSymbolTracker
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly List<SharedSymbol> _trackedSymbols;
|
||||||
|
private readonly bool _onlyTrackProvidedSymbols;
|
||||||
|
private readonly object _symbolLock = new object();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public UserDataSymbolTracker(ILogger logger, UserDataTrackerConfig config)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_trackedSymbols = config.TrackedSymbols?.ToList() ?? [];
|
||||||
|
_onlyTrackProvidedSymbols = config.OnlyTrackProvidedSymbols;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get currently tracked symbols
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public IEnumerable<SharedSymbol> GetTrackedSymbols()
|
||||||
|
{
|
||||||
|
lock (_symbolLock)
|
||||||
|
return _trackedSymbols.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether a symbol is in the tracked symbols list and should be processed
|
||||||
|
/// </summary>
|
||||||
|
public bool ShouldProcess(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
if (!_onlyTrackProvidedSymbols)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return _trackedSymbols.Any(y => y.TradingMode == symbol!.TradingMode && y.BaseAsset == symbol.BaseAsset && y.QuoteAsset == symbol.QuoteAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the tracked symbol list with potential new symbols
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateTrackedSymbols(IEnumerable<SharedSymbol> symbols, bool addByUser = false)
|
||||||
|
{
|
||||||
|
if (!addByUser && _onlyTrackProvidedSymbols)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_symbolLock)
|
||||||
|
{
|
||||||
|
foreach (var symbol in symbols.Distinct())
|
||||||
|
{
|
||||||
|
if (!_trackedSymbols.Any(x => x.TradingMode == symbol.TradingMode && x.BaseAsset == symbol.BaseAsset && x.QuoteAsset == symbol.QuoteAsset))
|
||||||
|
{
|
||||||
|
_trackedSymbols.Add(symbol);
|
||||||
|
_logger.LogDebug("Adding {TradingMode}.{BaseAsset}/{QuoteAsset} to symbol tracking list", symbol.TradingMode, symbol.BaseAsset, symbol.QuoteAsset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a symbol from the list
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveTrackedSymbol(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
lock (_symbolLock)
|
||||||
|
{
|
||||||
|
var symbolToRemove = _trackedSymbols.SingleOrDefault(x => x.TradingMode == symbol.TradingMode && x.BaseAsset == symbol.BaseAsset && x.QuoteAsset == symbol.QuoteAsset);
|
||||||
|
if (symbolToRemove != null)
|
||||||
|
_trackedSymbols.Remove(symbolToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
|
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
|
||||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -33,6 +34,11 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected abstract UserDataItemTracker[] DataTrackers { get; }
|
protected abstract UserDataItemTracker[] DataTrackers { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Symbol tracker
|
||||||
|
/// </summary>
|
||||||
|
protected internal UserDataSymbolTracker SymbolTracker { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? UserIdentifier { get; }
|
public string? UserIdentifier { get; }
|
||||||
|
|
||||||
@@ -51,6 +57,11 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Connected => DataTrackers.All(x => x.Connected);
|
public bool Connected => DataTrackers.All(x => x.Connected);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Currently tracked symbols
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<SharedSymbol> TrackedSymbols => SymbolTracker.GetTrackedSymbols();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -65,6 +76,7 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
|
SymbolTracker = new UserDataSymbolTracker(logger, config);
|
||||||
Exchange = exchange;
|
Exchange = exchange;
|
||||||
UserIdentifier = userIdentifier;
|
UserIdentifier = userIdentifier;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,21 +77,21 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
|
|
||||||
var trackers = new List<UserDataItemTracker>();
|
var trackers = new List<UserDataItemTracker>();
|
||||||
|
|
||||||
var balanceTracker = new BalanceTracker(logger, balanceRestClient, balanceSocketClient, accountType ?? SharedAccountType.PerpetualLinearFutures, config.BalancesConfig, exchangeParameters);
|
var balanceTracker = new BalanceTracker(logger, SymbolTracker, balanceRestClient, balanceSocketClient, accountType ?? SharedAccountType.PerpetualLinearFutures, config.BalancesConfig, exchangeParameters);
|
||||||
Balances = balanceTracker;
|
Balances = balanceTracker;
|
||||||
trackers.Add(balanceTracker);
|
trackers.Add(balanceTracker);
|
||||||
|
|
||||||
var orderTracker = new FuturesOrderTracker(logger, futuresOrderRestClient, futuresOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
var orderTracker = new FuturesOrderTracker(logger, SymbolTracker, futuresOrderRestClient, futuresOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||||
Orders = orderTracker;
|
Orders = orderTracker;
|
||||||
trackers.Add(orderTracker);
|
trackers.Add(orderTracker);
|
||||||
|
|
||||||
var positionTracker = new PositionTracker(logger, futuresOrderRestClient, positionSocketClient, config.PositionConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, WebsocketPositionUpdatesAreFullSnapshots, exchangeParameters);
|
var positionTracker = new PositionTracker(logger, SymbolTracker, futuresOrderRestClient, positionSocketClient, config.PositionConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, WebsocketPositionUpdatesAreFullSnapshots, exchangeParameters);
|
||||||
Positions = positionTracker;
|
Positions = positionTracker;
|
||||||
trackers.Add(positionTracker);
|
trackers.Add(positionTracker);
|
||||||
|
|
||||||
if (config.TrackTrades)
|
if (config.TrackTrades)
|
||||||
{
|
{
|
||||||
var tradeTracker = new FuturesUserTradeTracker(logger, futuresOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
var tradeTracker = new FuturesUserTradeTracker(logger, SymbolTracker, futuresOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||||
Trades = tradeTracker;
|
Trades = tradeTracker;
|
||||||
trackers.Add(tradeTracker);
|
trackers.Add(tradeTracker);
|
||||||
|
|
||||||
@@ -154,5 +154,30 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
interval = result ? TimeSpan.FromMinutes(30) : TimeSpan.FromMinutes(5);
|
interval = result ? TimeSpan.FromMinutes(30) : TimeSpan.FromMinutes(5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add symbols to the list of symbols for which data is being tracked
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbols">Symbols to add</param>
|
||||||
|
public void AddTrackedSymbolsAsync(IEnumerable<SharedSymbol> symbols)
|
||||||
|
{
|
||||||
|
if (symbols.Any(x => x.TradingMode == TradingMode.Spot))
|
||||||
|
throw new ArgumentException("Spot symbol not allowed in futures tracker", nameof(symbols));
|
||||||
|
|
||||||
|
SymbolTracker.UpdateTrackedSymbols(symbols, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a symbol from the list of symbols for which data is being tracked.
|
||||||
|
/// Note that the symbol will be added again if new data for that symbol is received, unless the OnlyTrackProvidedSymbols option has been set to true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">Symbol to remove</param>
|
||||||
|
public void RemoveTrackedSymbolAsync(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
SymbolTracker.RemoveTrackedSymbol(symbol);
|
||||||
|
|
||||||
|
((FuturesOrderTracker)Orders).ClearDataForSymbol(symbol);
|
||||||
|
((FuturesUserTradeTracker?)Trades)?.ClearDataForSymbol(symbol);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,17 +53,17 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
|
|
||||||
var trackers = new List<UserDataItemTracker>();
|
var trackers = new List<UserDataItemTracker>();
|
||||||
|
|
||||||
var balanceTracker = new BalanceTracker(logger, balanceRestClient, balanceSocketClient, SharedAccountType.Spot, config.BalancesConfig, exchangeParameters);
|
var balanceTracker = new BalanceTracker(logger, SymbolTracker, balanceRestClient, balanceSocketClient, SharedAccountType.Spot, config.BalancesConfig, exchangeParameters);
|
||||||
Balances = balanceTracker;
|
Balances = balanceTracker;
|
||||||
trackers.Add(balanceTracker);
|
trackers.Add(balanceTracker);
|
||||||
|
|
||||||
var orderTracker = new SpotOrderTracker(logger, spotOrderRestClient, spotOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
var orderTracker = new SpotOrderTracker(logger, SymbolTracker, spotOrderRestClient, spotOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||||
Orders = orderTracker;
|
Orders = orderTracker;
|
||||||
trackers.Add(orderTracker);
|
trackers.Add(orderTracker);
|
||||||
|
|
||||||
if (config.TrackTrades)
|
if (config.TrackTrades)
|
||||||
{
|
{
|
||||||
var tradeTracker = new SpotUserTradeTracker(logger, spotOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
var tradeTracker = new SpotUserTradeTracker(logger, SymbolTracker, spotOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||||
Trades = tradeTracker;
|
Trades = tradeTracker;
|
||||||
trackers.Add(tradeTracker);
|
trackers.Add(tradeTracker);
|
||||||
|
|
||||||
@@ -127,5 +127,30 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
interval = result ? TimeSpan.FromMinutes(30) : TimeSpan.FromMinutes(5);
|
interval = result ? TimeSpan.FromMinutes(30) : TimeSpan.FromMinutes(5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add symbols to the list of symbols for which data is being tracked
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbols">Symbols to add</param>
|
||||||
|
public void AddTrackedSymbolsAsync(IEnumerable<SharedSymbol> symbols)
|
||||||
|
{
|
||||||
|
if (symbols.Any(x => x.TradingMode != TradingMode.Spot))
|
||||||
|
throw new ArgumentException("Futures symbol not allowed in spot tracker", nameof(symbols));
|
||||||
|
|
||||||
|
SymbolTracker.UpdateTrackedSymbols(symbols, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a symbol from the list of symbols for which data is being tracked. Also removes stored data for that symbol.
|
||||||
|
/// Note that the symbol will be added again if new data for that symbol is received, unless the OnlyTrackProvidedSymbols option has been set to true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">Symbol to remove</param>
|
||||||
|
public void RemoveTrackedSymbolAsync(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
SymbolTracker.RemoveTrackedSymbol(symbol);
|
||||||
|
|
||||||
|
((SpotOrderTracker)Orders).ClearDataForSymbol(symbol);
|
||||||
|
((SpotUserTradeTracker?)Trades)?.ClearDataForSymbol(symbol);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,32 +5,32 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="12.1.0" />
|
<PackageReference Include="Binance.Net" Version="12.5.0" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="10.2.0" />
|
<PackageReference Include="Bitfinex.Net" Version="10.6.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="3.1.0" />
|
<PackageReference Include="BitMart.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="BloFin.Net" Version="2.1.1" />
|
<PackageReference Include="BloFin.Net" Version="2.5.0" />
|
||||||
<PackageReference Include="Bybit.Net" Version="6.1.0" />
|
<PackageReference Include="Bybit.Net" Version="6.5.0" />
|
||||||
<PackageReference Include="CoinEx.Net" Version="10.1.0" />
|
<PackageReference Include="CoinEx.Net" Version="10.5.0" />
|
||||||
<PackageReference Include="CoinW.Net" Version="2.1.1" />
|
<PackageReference Include="CoinW.Net" Version="2.5.0" />
|
||||||
<PackageReference Include="CryptoCom.Net" Version="3.1.0" />
|
<PackageReference Include="CryptoCom.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="DeepCoin.Net" Version="3.1.0" />
|
<PackageReference Include="DeepCoin.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="GateIo.Net" Version="3.1.0" />
|
<PackageReference Include="GateIo.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="HyperLiquid.Net" Version="3.2.0" />
|
<PackageReference Include="HyperLiquid.Net" Version="3.7.0" />
|
||||||
<PackageReference Include="JK.BingX.Net" Version="3.1.0" />
|
<PackageReference Include="JK.BingX.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="JK.Bitget.Net" Version="3.1.0" />
|
<PackageReference Include="JK.Bitget.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="JK.Mexc.Net" Version="4.1.0" />
|
<PackageReference Include="JK.Mexc.Net" Version="4.5.0" />
|
||||||
<PackageReference Include="JK.OKX.Net" Version="4.1.0" />
|
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
|
||||||
<PackageReference Include="Jkorf.Aster.Net" Version="2.1.0" />
|
<PackageReference Include="Jkorf.Aster.Net" Version="2.5.0" />
|
||||||
<PackageReference Include="JKorf.BitMEX.Net" Version="3.1.0" />
|
<PackageReference Include="JKorf.BitMEX.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="JKorf.Coinbase.Net" Version="3.1.0" />
|
<PackageReference Include="JKorf.Coinbase.Net" Version="3.5.1" />
|
||||||
<PackageReference Include="JKorf.HTX.Net" Version="8.1.0" />
|
<PackageReference Include="JKorf.HTX.Net" Version="8.5.0" />
|
||||||
<PackageReference Include="JKorf.Upbit.Net" Version="2.1.0" />
|
<PackageReference Include="JKorf.Upbit.Net" Version="2.5.0" />
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="7.1.0" />
|
<PackageReference Include="KrakenExchange.Net" Version="7.5.0" />
|
||||||
<PackageReference Include="Kucoin.Net" Version="8.1.0" />
|
<PackageReference Include="Kucoin.Net" Version="8.5.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
<PackageReference Include="Toobit.Net" Version="2.1.0" />
|
<PackageReference Include="Toobit.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="WhiteBit.Net" Version="3.1.0" />
|
<PackageReference Include="WhiteBit.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="XT.Net" Version="3.1.0" />
|
<PackageReference Include="XT.Net" Version="3.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -6,20 +6,20 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="12.1.0" />
|
<PackageReference Include="Binance.Net" Version="12.5.0" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="10.2.0" />
|
<PackageReference Include="Bitfinex.Net" Version="10.6.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="3.1.0" />
|
<PackageReference Include="BitMart.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="Bybit.Net" Version="6.1.0" />
|
<PackageReference Include="Bybit.Net" Version="6.5.0" />
|
||||||
<PackageReference Include="CoinEx.Net" Version="10.1.0" />
|
<PackageReference Include="CoinEx.Net" Version="10.5.0" />
|
||||||
<PackageReference Include="CryptoCom.Net" Version="3.1.0" />
|
<PackageReference Include="CryptoCom.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="GateIo.Net" Version="3.1.0" />
|
<PackageReference Include="GateIo.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="JK.Bitget.Net" Version="3.1.0" />
|
<PackageReference Include="JK.Bitget.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="JK.Mexc.Net" Version="4.1.0" />
|
<PackageReference Include="JK.Mexc.Net" Version="4.5.0" />
|
||||||
<PackageReference Include="JK.OKX.Net" Version="4.1.0" />
|
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
|
||||||
<PackageReference Include="JKorf.Coinbase.Net" Version="3.1.0" />
|
<PackageReference Include="JKorf.Coinbase.Net" Version="3.5.1" />
|
||||||
<PackageReference Include="JKorf.HTX.Net" Version="8.1.0" />
|
<PackageReference Include="JKorf.HTX.Net" Version="8.5.0" />
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="7.1.0" />
|
<PackageReference Include="KrakenExchange.Net" Version="7.5.0" />
|
||||||
<PackageReference Include="Kucoin.Net" Version="8.1.0" />
|
<PackageReference Include="Kucoin.Net" Version="8.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="12.1.0" />
|
<PackageReference Include="Binance.Net" Version="12.5.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="3.1.0" />
|
<PackageReference Include="BitMart.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="JK.OKX.Net" Version="4.1.0" />
|
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -67,6 +67,26 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
|||||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 10.6.0 - 16 Feb 2026
|
||||||
|
* Updated symbol tracking logic on UserDataTracker, now is per UserDataTracker instead of per topic
|
||||||
|
* Added check for startTime filter for polling being to close to current time which can cause issues if time isn't in sync with server
|
||||||
|
* Added AddTrackedSymbolsAsync and RemoveTrackedSymbolAsync methods to UserDataTracker
|
||||||
|
* Added check SymbolOrderBook is still alive when trying to add updates to prevent unnoticed growing in the background when subscription isn't closed while book is
|
||||||
|
|
||||||
|
* Version 10.5.4 - 12 Feb 2026
|
||||||
|
* Fixed type check ExchangeParameters GetValue
|
||||||
|
* Fixed bug in polling time filter for UserDataTracker item
|
||||||
|
|
||||||
|
* Version 10.5.3 - 11 Feb 2026
|
||||||
|
* Fixed orders getting incorrectly set to canceled state for UserDataTracker spot and futures orders
|
||||||
|
* Added check EnumConverter to detect undefined int value parsing
|
||||||
|
|
||||||
|
* Version 10.5.2 - 10 Feb 2026
|
||||||
|
* Added check for subscribe queries with TimeoutBehavior.Success to complete when subscription has received update
|
||||||
|
* Added call to ApiClient.HandleUnhandledMessage when no websocket message processor is found based on topic to allow additional processing
|
||||||
|
* Combined websocket connection subscribe and re-subscribe logic
|
||||||
|
* Set websocket query completed after setting Result
|
||||||
|
|
||||||
* Version 10.5.1 - 10 Feb 2026
|
* Version 10.5.1 - 10 Feb 2026
|
||||||
* Fixed trading mode selection for futures listen key methods in FuturesUserDataTracker
|
* Fixed trading mode selection for futures listen key methods in FuturesUserDataTracker
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user