diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesOrderTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesOrderTracker.cs index 0b7f023..3c52427 100644 --- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesOrderTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesOrderTracker.cs @@ -271,20 +271,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers _openOrderNotReturnedTimes[order.OrderId] += 1; } + var updatedPollTime = DateTime.UtcNow; foreach (var symbol in _symbols.ToList()) { - // Determine the timestamp from which we need to check order status - // Use the timestamp we last know the correct state of the data - var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime; - // If we're tracking 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 != null && trackedOrdersMinOpenTime < fromTimeOrders) - fromTimeOrders = trackedOrdersMinOpenTime.Value.AddMilliseconds(-1); + DateTime? fromTimeOrders = GetClosedOrdersRequestStartTime(symbol); - var updatedPollTime = DateTime.UtcNow; var closedOrdersResult = await _restClient.GetClosedFuturesOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false); if (!closedOrdersResult.Success) { @@ -296,14 +287,12 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers } else { - _lastDataTimeBeforeDisconnect = null; - _lastPollTime = updatedPollTime; - // Filter orders to only include where close time is after the start time var relevantOrders = closedOrdersResult.Data.Where(x => (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.UpdateTime == null) // Unknown time + || (Values.Any(e => e.OrderId == x.OrderId && x.Status == SharedOrderStatus.Open)) // Or we're currently tracking this open order ).ToArray(); // Check for orders which are no longer returned in either open/closed and assume they're canceled without fill @@ -335,7 +324,57 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers } } + if (!anyError) + { + _lastPollTime = updatedPollTime; + _lastDataTimeBeforeDisconnect = null; + } + 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"; + } + + _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/FuturesUserTradeTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesUserTradeTracker.cs index 93291ea..a0f8d8f 100644 --- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesUserTradeTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/FuturesUserTradeTracker.cs @@ -55,10 +55,10 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers protected override async Task DoPollAsync() { var anyError = false; + var fromTimeTrades = GetTradesRequestStartTime(); + var updatedPollTime = DateTime.UtcNow; foreach (var symbol in _symbols) { - var fromTimeTrades = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime; - var updatedPollTime = DateTime.UtcNow; var tradesResult = await _restClient.GetFuturesUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false); if (!tradesResult.Success) { @@ -80,9 +80,46 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers } } + if (!anyError) + { + _lastDataTimeBeforeDisconnect = null; + _lastPollTime = updatedPollTime; + } + 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"; + } + + _logger.LogTrace("{DataType} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}", DataType, source, fromTime); + return fromTime!.Value; + } + /// protected override Task> DoSubscribeAsync(string? listenKey) { diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotOrderTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotOrderTracker.cs index 1405f77..e16254f 100644 --- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotOrderTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotOrderTracker.cs @@ -282,20 +282,12 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers _openOrderNotReturnedTimes[order.OrderId] += 1; } + var updatedPollTime = DateTime.UtcNow; foreach (var symbol in _symbols.ToList()) { - // Determine the timestamp from which we need to check order status - // Use the timestamp we last know the correct state of the data - var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime; - // If we're tracking 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 != null && trackedOrdersMinOpenTime < fromTimeOrders) - fromTimeOrders = trackedOrdersMinOpenTime.Value.AddMilliseconds(-1); + DateTime? fromTimeOrders = GetClosedOrdersRequestStartTime(symbol); - var updatedPollTime = DateTime.UtcNow; + var closedOrdersResult = await _restClient.GetClosedSpotOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false); if (!closedOrdersResult.Success) { @@ -307,14 +299,12 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers } else { - _lastDataTimeBeforeDisconnect = null; - _lastPollTime = updatedPollTime; - // Filter orders to only include where close time is after the start time var relevantOrders = closedOrdersResult.Data.Where(x => (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.UpdateTime == null) // Unknown time + || (Values.Any(e => e.OrderId == x.OrderId && x.Status == SharedOrderStatus.Open)) // Or we're currently tracking this open order ).ToArray(); // Check for orders which are no longer returned in either open/closed and assume they're canceled without fill @@ -346,7 +336,57 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers } } + if (!anyError) + { + _lastDataTimeBeforeDisconnect = null; + _lastPollTime = updatedPollTime; + } + 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"; + } + + _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/SpotUserTradeTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotUserTradeTracker.cs index 0578ac6..cf39fb8 100644 --- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotUserTradeTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/SpotUserTradeTracker.cs @@ -55,10 +55,10 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers protected override async Task DoPollAsync() { var anyError = false; + var fromTimeTrades = GetTradesRequestStartTime(); + var updatedPollTime = DateTime.UtcNow; foreach (var symbol in _symbols) { - var fromTimeTrades = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime; - var updatedPollTime = DateTime.UtcNow; var tradesResult = await _restClient.GetSpotUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false); if (!tradesResult.Success) { @@ -70,8 +70,6 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers } 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 var relevantTrades = tradesResult.Data.Where(x => x.Timestamp >= _startTime || (GetTrackedOrderIds?.Invoke() ?? []).Any(o => o == x.OrderId)).ToArray(); @@ -80,9 +78,46 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers } } + if (!anyError) + { + _lastDataTimeBeforeDisconnect = null; + _lastPollTime = updatedPollTime; + } + 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"; + } + + _logger.LogTrace("{DataType} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}", DataType, source, fromTime); + return fromTime!.Value; + } + /// protected override Task> DoSubscribeAsync(string? listenKey) { diff --git a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/UserDataItemTracker.cs b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/UserDataItemTracker.cs index cd01736..aa9ba0d 100644 --- a/CryptoExchange.Net/Trackers/UserData/ItemTrackers/UserDataItemTracker.cs +++ b/CryptoExchange.Net/Trackers/UserData/ItemTrackers/UserDataItemTracker.cs @@ -372,6 +372,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers { toRemove ??= new List(); 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))