diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj
index 5da4874..dea73d4 100644
--- a/CryptoExchange.Net/CryptoExchange.Net.csproj
+++ b/CryptoExchange.Net/CryptoExchange.Net.csproj
@@ -6,9 +6,9 @@
CryptoExchange.Net
JKorf
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.
- 10.5.3
- 10.5.3
- 10.5.3
+ 10.6.0
+ 10.6.0
+ 10.6.0
false
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
git
diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs
index 9b62eb2..766cf58 100644
--- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs
+++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs
@@ -448,6 +448,9 @@ namespace CryptoExchange.Net.OrderBook
DateTime? serverDataTime = 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(
new OrderBookSnapshot
{
@@ -475,6 +478,9 @@ namespace CryptoExchange.Net.OrderBook
DateTime? serverDataTime = 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(
new OrderBookUpdate
{
@@ -505,6 +511,9 @@ namespace CryptoExchange.Net.OrderBook
DateTime? serverDataTime = 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(
new OrderBookUpdate
{
@@ -531,6 +540,9 @@ namespace CryptoExchange.Net.OrderBook
DateTime? serverDataTime = 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 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
/// The sequence number of the message if it's a separate message with separate number
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 });
_queueEvent.Set();
}
diff --git a/CryptoExchange.Net/Sockets/Default/SocketConnection.cs b/CryptoExchange.Net/Sockets/Default/SocketConnection.cs
index d1e47a0..e2f4915 100644
--- a/CryptoExchange.Net/Sockets/Default/SocketConnection.cs
+++ b/CryptoExchange.Net/Sockets/Default/SocketConnection.cs
@@ -1103,6 +1103,12 @@ namespace CryptoExchange.Net.Sockets.Default
return CallResult.SuccessResult;
}
+ ///
+ /// Try to subscribe a new subscription by sending the subscribe query and wait for the result as needed
+ ///
+ /// The subscription
+ /// Whether this is a new subscription, or an existing subscription (resubscribing on reconnected socket)
+ /// Cancellation token
protected internal async Task TrySubscribeAsync(Subscription subscription, bool newSubscription, CancellationToken subCancelToken)
{
subscription.ConnectionInvocations = 0;
diff --git a/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserDataTracker.cs b/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserDataTracker.cs
index 2cf7d00..e98282c 100644
--- a/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserDataTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserDataTracker.cs
@@ -16,13 +16,6 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
///
bool Connected { get; }
- ///
- /// 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.
- ///
- IEnumerable TrackedSymbols { get; }
-
///
/// On connection status change. Might trigger multiple times with the same status depending on the underlying subscriptions.
///
diff --git a/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserFuturesDataTracker.cs b/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserFuturesDataTracker.cs
index 3c429eb..fcc669e 100644
--- a/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserFuturesDataTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserFuturesDataTracker.cs
@@ -2,6 +2,7 @@
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Trackers.UserData.Objects;
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
@@ -26,6 +27,13 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
///
public string Exchange { get; }
+ ///
+ /// 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.
+ ///
+ IEnumerable TrackedSymbols { get; }
+
///
/// Balances tracker
///
@@ -57,5 +65,18 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
///
///
Task StopAsync();
+
+ ///
+ /// Add symbols to the list of symbols for which data is being tracked
+ ///
+ /// Symbols to add
+ void AddTrackedSymbolsAsync(IEnumerable symbols);
+
+ ///
+ /// 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.
+ ///
+ /// Symbol to remove
+ void RemoveTrackedSymbolAsync(SharedSymbol symbol);
}
}
\ No newline at end of file
diff --git a/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserSpotDataTracker.cs b/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserSpotDataTracker.cs
index 35612d0..de498bd 100644
--- a/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserSpotDataTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/Interfaces/IUserSpotDataTracker.cs
@@ -2,6 +2,7 @@
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Trackers.UserData.Objects;
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
@@ -26,6 +27,13 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
///
public string Exchange { get; }
+ ///
+ /// 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.
+ ///
+ IEnumerable TrackedSymbols { get; }
+
///
/// Balances tracker
///
@@ -53,5 +61,18 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
///
///
Task StopAsync();
+
+ ///
+ /// Add symbols to the list of symbols for which data is being tracked
+ ///
+ /// Symbols to add
+ void AddTrackedSymbolsAsync(IEnumerable symbols);
+
+ ///
+ /// 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.
+ ///
+ /// Symbol to remove
+ void RemoveTrackedSymbolAsync(SharedSymbol symbol);
}
}
\ No newline at end of file
diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/BalanceTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/BalanceTracker.cs
index 66f24fd..3895b87 100644
--- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/BalanceTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/BalanceTracker.cs
@@ -22,12 +22,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
///
public BalanceTracker(
ILogger logger,
+ UserDataSymbolTracker symbolTracker,
IBalanceRestClient restClient,
IBalanceSocketClient? socketClient,
SharedAccountType accountType,
TrackerItemConfig config,
ExchangeParameters? exchangeParameters = null
- ) : base(logger, UserDataType.Balances, restClient.Exchange, config, false, null)
+ ) : base(logger, symbolTracker, UserDataType.Balances, restClient.Exchange, config)
{
if (_socketClient == null)
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesOrderTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesOrderTracker.cs
index 3c52427..72b443d 100644
--- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesOrderTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesOrderTracker.cs
@@ -28,13 +28,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
///
public FuturesOrderTracker(
ILogger logger,
+ UserDataSymbolTracker symbolTracker,
IFuturesOrderRestClient restClient,
IFuturesOrderSocketClient? socketClient,
TrackerItemConfig config,
IEnumerable symbols,
bool onlyTrackProvidedSymbols,
ExchangeParameters? exchangeParameters = null
- ) : base(logger, UserDataType.Orders, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
+ ) : base(logger, symbolTracker, UserDataType.Orders, restClient.Exchange, config)
{
if (_socketClient == null)
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
@@ -46,6 +47,20 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
_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 _);
+ }
+ }
+ }
+
///
protected override bool Update(SharedFuturesOrder existingItem, SharedFuturesOrder updateItem)
{
@@ -234,7 +249,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
}
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);
if (!openOrdersResult.Success)
@@ -272,7 +287,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
}
var updatedPollTime = DateTime.UtcNow;
- foreach (var symbol in _symbols.ToList())
+ foreach (var symbol in _symbolTracker.GetTrackedSymbols())
{
DateTime? fromTimeOrders = GetClosedOrdersRequestStartTime(symbol);
@@ -373,7 +388,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
source = "StartTime";
}
- _logger.LogTrace("{DataType} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}", DataType, source, fromTime);
+ 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;
}
}
diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesUserTradeTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesUserTradeTracker.cs
index a0f8d8f..208928b 100644
--- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesUserTradeTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesUserTradeTracker.cs
@@ -26,13 +26,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
///
public FuturesUserTradeTracker(
ILogger logger,
+ UserDataSymbolTracker symbolTracker,
IFuturesOrderRestClient restClient,
IUserTradeSocketClient? socketClient,
TrackerItemConfig config,
IEnumerable symbols,
bool onlyTrackProvidedSymbols,
ExchangeParameters? exchangeParameters = null
- ) : base(logger, UserDataType.Trades, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
+ ) : base(logger, symbolTracker, UserDataType.Trades, restClient.Exchange, config)
{
if (_socketClient == null)
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
@@ -42,6 +43,20 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
_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 _);
+ }
+ }
+ }
+
///
protected override string GetKey(SharedUserTrade item) => item.Id;
///
@@ -57,7 +72,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
var anyError = false;
var fromTimeTrades = GetTradesRequestStartTime();
var updatedPollTime = DateTime.UtcNow;
- foreach (var symbol in _symbols)
+ foreach (var symbol in _symbolTracker.GetTrackedSymbols())
{
var tradesResult = await _restClient.GetFuturesUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
if (!tradesResult.Success)
@@ -116,6 +131,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
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;
}
diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/PositionTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/PositionTracker.cs
index 1701c69..092c01f 100644
--- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/PositionTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/PositionTracker.cs
@@ -29,6 +29,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
///
public PositionTracker(
ILogger logger,
+ UserDataSymbolTracker symbolTracker,
IFuturesOrderRestClient restClient,
IPositionSocketClient? socketClient,
TrackerItemConfig config,
@@ -36,7 +37,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
bool onlyTrackProvidedSymbols,
bool websocketPositionUpdatesAreFullSnapshots,
ExchangeParameters? exchangeParameters = null
- ) : base(logger, UserDataType.Positions, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
+ ) : base(logger, symbolTracker, UserDataType.Positions, restClient.Exchange, config)
{
if (_socketClient == null)
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
@@ -118,9 +119,9 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
{
toRemove ??= new List();
toRemove.Add(item);
+ _logger.LogTrace("Ignoring {DataType} update for {Key}, no SharedSymbol set", DataType, item.Symbol);
}
- else if (_onlyTrackProvidedSymbols
- && !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
+ else if (!_symbolTracker.ShouldProcess(symbolModel.SharedSymbol))
{
toRemove ??= new List();
toRemove.Add(item);
@@ -131,8 +132,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
if (toRemove != null)
@event = @event.Except(toRemove).ToArray();
- if (!_onlyTrackProvidedSymbols)
- UpdateSymbolsList(@event.Where(x => x.PositionSize > 0).OfType().Select(x => x.SharedSymbol!));
+ _symbolTracker.UpdateTrackedSymbols(@event.Where(x => x.PositionSize > 0).OfType().Select(x => x.SharedSymbol!));
// Update local store
diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotOrderTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotOrderTracker.cs
index e16254f..4264942 100644
--- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotOrderTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotOrderTracker.cs
@@ -28,13 +28,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
///
public SpotOrderTracker(
ILogger logger,
+ UserDataSymbolTracker symbolTracker,
ISpotOrderRestClient restClient,
ISpotOrderSocketClient? socketClient,
TrackerItemConfig config,
IEnumerable symbols,
bool onlyTrackProvidedSymbols,
ExchangeParameters? exchangeParameters = null
- ) : base(logger, UserDataType.Orders, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
+ ) : base(logger, symbolTracker, UserDataType.Orders, restClient.Exchange, config)
{
if (_socketClient == null)
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
@@ -46,6 +47,19 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
_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 _);
+ }
+ }
+ }
+
///
protected override bool Update(SharedSpotOrder existingItem, SharedSpotOrder updateItem)
{
@@ -245,7 +259,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
}
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);
if (!openOrdersResult.Success)
@@ -283,7 +297,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
}
var updatedPollTime = DateTime.UtcNow;
- foreach (var symbol in _symbols.ToList())
+ foreach (var symbol in _symbolTracker.GetTrackedSymbols())
{
DateTime? fromTimeOrders = GetClosedOrdersRequestStartTime(symbol);
@@ -385,7 +399,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
source = "StartTime";
}
- _logger.LogTrace("{DataType} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}", DataType, source, fromTime);
+ 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;
}
}
diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotUserTradeTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotUserTradeTracker.cs
index cf39fb8..21eeae2 100644
--- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotUserTradeTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotUserTradeTracker.cs
@@ -26,13 +26,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
///
public SpotUserTradeTracker(
ILogger logger,
+ UserDataSymbolTracker symbolTracker,
ISpotOrderRestClient restClient,
IUserTradeSocketClient? socketClient,
TrackerItemConfig config,
IEnumerable symbols,
bool onlyTrackProvidedSymbols,
ExchangeParameters? exchangeParameters = null
- ) : base(logger, UserDataType.Trades, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
+ ) : base(logger, symbolTracker, UserDataType.Trades, restClient.Exchange, config)
{
if (_socketClient == null)
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
@@ -42,6 +43,19 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
_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 _);
+ }
+ }
+ }
+
///
protected override string GetKey(SharedUserTrade item) => item.Id;
///
@@ -57,7 +71,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
var anyError = false;
var fromTimeTrades = GetTradesRequestStartTime();
var updatedPollTime = DateTime.UtcNow;
- foreach (var symbol in _symbols)
+ foreach (var symbol in _symbolTracker.GetTrackedSymbols())
{
var tradesResult = await _restClient.GetSpotUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
if (!tradesResult.Success)
@@ -114,6 +128,12 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
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;
}
diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/UserDataItemTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/UserDataItemTracker.cs
index aa9ba0d..77f8de1 100644
--- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/UserDataItemTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/UserDataItemTracker.cs
@@ -203,21 +203,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
///
protected ConcurrentDictionary _store = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase);
///
- /// Tracked symbols list
- ///
- protected readonly List _symbols;
- ///
- /// Symbol lock
- ///
- protected object _symbolLock = new object();
- ///
- /// Only track provided symbols setting
- ///
- protected bool _onlyTrackProvidedSymbols;
- ///
/// Is SharedSymbol model
///
protected bool _isSymbolModel;
+ ///
+ /// Symbol tracker
+ ///
+
+ protected readonly UserDataSymbolTracker _symbolTracker;
///
public T[] Values
@@ -240,22 +233,23 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
///
public event Func, Task>? OnUpdate;
- ///
- public IEnumerable TrackedSymbols => _symbols;
///
/// ctor
///
- public UserDataItemTracker(ILogger logger, UserDataType dataType, string exchange, TrackerItemConfig config, bool onlyTrackProvidedSymbols, IEnumerable? 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;
_pollIntervalConnected = config.PollIntervalConnected;
_pollAtStart = config.PollAtStart;
_retentionTime = config is TrackerTimedItemConfig timeConfig ? timeConfig.RetentionTime : TimeSpan.MaxValue;
_isSymbolModel = typeof(T).IsSubclassOf(typeof(SharedSymbolModel));
+ _symbolTracker = symbolTracker;
}
///
@@ -334,26 +328,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
/// Get the age of an item
///
protected virtual TimeSpan GetAge(DateTime time, T item) => TimeSpan.Zero;
-
- ///
- /// Update the tracked symbol list with potential new symbols
- ///
- ///
- protected void UpdateSymbolsList(IEnumerable 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);
- }
- }
- }
- }
-
+
///
/// Handle an update
///
@@ -374,8 +349,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
toRemove.Add(item);
_logger.LogWarning("Ignoring {DataType} update for {Key}, no SharedSymbol set", DataType, GetKey(item));
}
- else if (_onlyTrackProvidedSymbols
- && !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
+ else if (!_symbolTracker.ShouldProcess(symbolModel.SharedSymbol))
{
toRemove ??= new List();
toRemove.Add(item);
@@ -386,8 +360,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
if (toRemove != null)
@event = @event.Except(toRemove).ToArray();
- if (!_onlyTrackProvidedSymbols)
- UpdateSymbolsList(@event.OfType().Select(x => x.SharedSymbol!));
+ _symbolTracker.UpdateTrackedSymbols(@event.OfType().Select(x => x.SharedSymbol!));
}
// Update local store
diff --git a/CryptoExchange.Net/Trackers/UserData/Objects/UserDataSymbolTracker.cs b/CryptoExchange.Net/Trackers/UserData/Objects/UserDataSymbolTracker.cs
new file mode 100644
index 0000000..9ff50f8
--- /dev/null
+++ b/CryptoExchange.Net/Trackers/UserData/Objects/UserDataSymbolTracker.cs
@@ -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
+{
+ ///
+ /// Tracker for symbols used in UserDataTracker
+ ///
+ public class UserDataSymbolTracker
+ {
+ private readonly ILogger _logger;
+ private readonly List _trackedSymbols;
+ private readonly bool _onlyTrackProvidedSymbols;
+ private readonly object _symbolLock = new object();
+
+ ///
+ /// ctor
+ ///
+ public UserDataSymbolTracker(ILogger logger, UserDataTrackerConfig config)
+ {
+ _logger = logger;
+ _trackedSymbols = config.TrackedSymbols?.ToList() ?? [];
+ _onlyTrackProvidedSymbols = config.OnlyTrackProvidedSymbols;
+ }
+
+ ///
+ /// Get currently tracked symbols
+ ///
+ ///
+ public IEnumerable GetTrackedSymbols()
+ {
+ lock (_symbolLock)
+ return _trackedSymbols.ToList();
+ }
+
+ ///
+ /// Check whether a symbol is in the tracked symbols list and should be processed
+ ///
+ 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);
+ }
+
+ ///
+ /// Update the tracked symbol list with potential new symbols
+ ///
+ public void UpdateTrackedSymbols(IEnumerable 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);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Remove a symbol from the list
+ ///
+ 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);
+ }
+ }
+ }
+}
diff --git a/CryptoExchange.Net/Trackers/UserData/UserDataTracker.cs b/CryptoExchange.Net/Trackers/UserData/UserDataTracker.cs
index 84131e0..b38bcb2 100644
--- a/CryptoExchange.Net/Trackers/UserData/UserDataTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/UserDataTracker.cs
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Objects;
+using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
using CryptoExchange.Net.Trackers.UserData.Objects;
using Microsoft.Extensions.Logging;
@@ -33,6 +34,11 @@ namespace CryptoExchange.Net.Trackers.UserData
///
protected abstract UserDataItemTracker[] DataTrackers { get; }
+ ///
+ /// Symbol tracker
+ ///
+ protected internal UserDataSymbolTracker SymbolTracker { get; }
+
///
public string? UserIdentifier { get; }
@@ -51,6 +57,11 @@ namespace CryptoExchange.Net.Trackers.UserData
///
public bool Connected => DataTrackers.All(x => x.Connected);
+ ///
+ /// Currently tracked symbols
+ ///
+ public IEnumerable TrackedSymbols => SymbolTracker.GetTrackedSymbols();
+
///
/// ctor
///
@@ -65,6 +76,7 @@ namespace CryptoExchange.Net.Trackers.UserData
_logger = logger;
+ SymbolTracker = new UserDataSymbolTracker(logger, config);
Exchange = exchange;
UserIdentifier = userIdentifier;
}
diff --git a/CryptoExchange.Net/Trackers/UserData/UserFuturesDataTracker.cs b/CryptoExchange.Net/Trackers/UserData/UserFuturesDataTracker.cs
index 355e281..b7366c5 100644
--- a/CryptoExchange.Net/Trackers/UserData/UserFuturesDataTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/UserFuturesDataTracker.cs
@@ -77,21 +77,21 @@ namespace CryptoExchange.Net.Trackers.UserData
var trackers = new List();
- 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;
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;
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;
trackers.Add(positionTracker);
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;
trackers.Add(tradeTracker);
@@ -154,5 +154,30 @@ namespace CryptoExchange.Net.Trackers.UserData
interval = result ? TimeSpan.FromMinutes(30) : TimeSpan.FromMinutes(5);
}
}
+
+ ///
+ /// Add symbols to the list of symbols for which data is being tracked
+ ///
+ /// Symbols to add
+ public void AddTrackedSymbolsAsync(IEnumerable 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// Symbol to remove
+ public void RemoveTrackedSymbolAsync(SharedSymbol symbol)
+ {
+ SymbolTracker.RemoveTrackedSymbol(symbol);
+
+ ((FuturesOrderTracker)Orders).ClearDataForSymbol(symbol);
+ ((FuturesUserTradeTracker?)Trades)?.ClearDataForSymbol(symbol);
+ }
}
}
diff --git a/CryptoExchange.Net/Trackers/UserData/UserSpotDataTracker.cs b/CryptoExchange.Net/Trackers/UserData/UserSpotDataTracker.cs
index 6856bca..b71a700 100644
--- a/CryptoExchange.Net/Trackers/UserData/UserSpotDataTracker.cs
+++ b/CryptoExchange.Net/Trackers/UserData/UserSpotDataTracker.cs
@@ -53,17 +53,17 @@ namespace CryptoExchange.Net.Trackers.UserData
var trackers = new List();
- 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;
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;
trackers.Add(orderTracker);
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;
trackers.Add(tradeTracker);
@@ -127,5 +127,30 @@ namespace CryptoExchange.Net.Trackers.UserData
interval = result ? TimeSpan.FromMinutes(30) : TimeSpan.FromMinutes(5);
}
}
+
+ ///
+ /// Add symbols to the list of symbols for which data is being tracked
+ ///
+ /// Symbols to add
+ public void AddTrackedSymbolsAsync(IEnumerable 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// Symbol to remove
+ public void RemoveTrackedSymbolAsync(SharedSymbol symbol)
+ {
+ SymbolTracker.RemoveTrackedSymbol(symbol);
+
+ ((SpotOrderTracker)Orders).ClearDataForSymbol(symbol);
+ ((SpotUserTradeTracker?)Trades)?.ClearDataForSymbol(symbol);
+ }
}
}
diff --git a/README.md b/README.md
index 3009521..863d4f0 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,16 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes
+* Version 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