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

Compare commits

...

3 Commits

Author SHA1 Message Date
Jkorf f39d9f7cfb Updated to version 10.5.4 2026-02-12 11:43:20 +01:00
Jkorf 9fab8faa45 Fixed bug in polling time filter for UserDataTracker items 2026-02-12 11:35:04 +01:00
Jkorf 226f175343 Fixed type check ExchangeParameters GetValue 2026-02-11 14:50:39 +01:00
8 changed files with 197 additions and 37 deletions
+3 -3
View File
@@ -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.3</PackageVersion> <PackageVersion>10.5.4</PackageVersion>
<AssemblyVersion>10.5.3</AssemblyVersion> <AssemblyVersion>10.5.4</AssemblyVersion>
<FileVersion>10.5.3</FileVersion> <FileVersion>10.5.4</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>
@@ -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);
@@ -271,20 +271,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
_openOrderNotReturnedTimes[order.OrderId] += 1; _openOrderNotReturnedTimes[order.OrderId] += 1;
} }
var updatedPollTime = DateTime.UtcNow;
foreach (var symbol in _symbols.ToList()) foreach (var symbol in _symbols.ToList())
{ {
// Determine the timestamp from which we need to check order status DateTime? fromTimeOrders = GetClosedOrdersRequestStartTime(symbol);
// 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);
var updatedPollTime = DateTime.UtcNow;
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)
{ {
@@ -296,14 +287,12 @@ 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
@@ -335,7 +324,57 @@ 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";
}
_logger.LogTrace("{DataType} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}", DataType, source, fromTime);
return fromTime!.Value;
}
} }
} }
@@ -55,10 +55,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;
var fromTimeTrades = GetTradesRequestStartTime();
var updatedPollTime = DateTime.UtcNow;
foreach (var symbol in _symbols) 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); var tradesResult = await _restClient.GetFuturesUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
if (!tradesResult.Success) if (!tradesResult.Success)
{ {
@@ -80,9 +80,46 @@ 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";
}
_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)
{ {
@@ -282,20 +282,12 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
_openOrderNotReturnedTimes[order.OrderId] += 1; _openOrderNotReturnedTimes[order.OrderId] += 1;
} }
var updatedPollTime = DateTime.UtcNow;
foreach (var symbol in _symbols.ToList()) foreach (var symbol in _symbols.ToList())
{ {
// Determine the timestamp from which we need to check order status DateTime? fromTimeOrders = GetClosedOrdersRequestStartTime(symbol);
// 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);
var updatedPollTime = DateTime.UtcNow;
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)
{ {
@@ -307,14 +299,12 @@ 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
@@ -346,7 +336,57 @@ 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";
}
_logger.LogTrace("{DataType} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}", DataType, source, fromTime);
return fromTime!.Value;
}
} }
} }
@@ -55,10 +55,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;
var fromTimeTrades = GetTradesRequestStartTime();
var updatedPollTime = DateTime.UtcNow;
foreach (var symbol in _symbols) 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); var tradesResult = await _restClient.GetSpotUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
if (!tradesResult.Success) if (!tradesResult.Success)
{ {
@@ -70,8 +70,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 +78,46 @@ 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";
}
_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)
{ {
@@ -372,6 +372,7 @@ 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 (_onlyTrackProvidedSymbols
&& !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset)) && !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
+4
View File
@@ -67,6 +67,10 @@ 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.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 * Version 10.5.3 - 11 Feb 2026
* Fixed orders getting incorrectly set to canceled state for UserDataTracker spot and futures orders * Fixed orders getting incorrectly set to canceled state for UserDataTracker spot and futures orders
* Added check EnumConverter to detect undefined int value parsing * Added check EnumConverter to detect undefined int value parsing