1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2026-02-16 14:13:46 +00:00

Compare commits

..

No commits in common. "1471a4733fcef0d8956f44e60316cd69b9bd9005" and "87dd2d9d40322d6e22caf4cccc55c5e8e0608a0d" have entirely different histories.

30 changed files with 260 additions and 2204 deletions

View File

@ -1,465 +0,0 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
using System.Linq;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class ExchangeSymbolCacheTests
{
private SharedSpotSymbol[] CreateTestSymbols()
{
return new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("BTC", "EUR", "BTCEUR", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
new SharedSpotSymbol("XRP", "USDT", "XRPUSDT", false, TradingMode.Spot)
};
}
private SharedSpotSymbol[] CreateFuturesSymbols()
{
return new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT-PERP", true, TradingMode.PerpetualLinear),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT-PERP", true, TradingMode.PerpetualLinear)
};
}
[Test]
public void UpdateSymbolInfo_NewTopic_Should_AddToCache()
{
// arrange
var topicId = "NewExchange";
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(hasCached, Is.True);
}
[Test]
public void UpdateSymbolInfo_Should_StoreAllSymbols()
{
// arrange
var topicId = "ExchangeWithSymbols";
var symbols = CreateTestSymbols();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// assert
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCEUR"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHBTC"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "XRPUSDT"), Is.True);
}
[Test]
public void UpdateSymbolInfo_CalledTwiceWithinAnHour_Should_NotUpdate()
{
// arrange
var topicId = "ExchangeNoUpdate";
var initialSymbols = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot)
};
var updatedSymbols = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot)
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, initialSymbols);
ExchangeSymbolCache.UpdateSymbolInfo(topicId, updatedSymbols);
// assert - should still have only the initial symbol since less than 60 minutes passed
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
// The second update should not have been applied
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.False);
}
[Test]
public void UpdateSymbolInfo_WithEmptyArray_Should_CreateEmptyCache()
{
// arrange
var topicId = "EmptyExchange";
var symbols = Array.Empty<SharedSpotSymbol>();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var hasCached = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(hasCached, Is.False);
}
[Test]
public void HasCached_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.HasCached(nonExistentTopic);
// assert
Assert.That(result, Is.False);
}
[Test]
public void HasCached_ExistingTopicWithSymbols_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeWithData";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(result, Is.True);
}
[Test]
public void HasCached_ExistingTopicWithNoSymbols_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoData";
ExchangeSymbolCache.UpdateSymbolInfo(topicId, Array.Empty<SharedSpotSymbol>());
// act
var result = ExchangeSymbolCache.HasCached(topicId);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_ByName_ExistingSymbol_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeSupports";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT");
// assert
Assert.That(result, Is.True);
}
[Test]
public void SupportsSymbol_ByName_NonExistingSymbol_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoSupport";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "LINKUSDT");
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_ByName_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "BTCUSDT");
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_ExistingSymbol_Should_ReturnTrue()
{
// arrange
var topicId = "ExchangeSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
// assert
Assert.That(result, Is.True);
}
[Test]
public void SupportsSymbol_BySharedSymbol_NonExistingSymbol_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeNoSharedSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "LINK", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_DifferentTradingMode_Should_ReturnFalse()
{
// arrange
var topicId = "ExchangeDifferentMode";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
var sharedSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void SupportsSymbol_BySharedSymbol_NonExistentTopic_Should_ReturnFalse()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, sharedSymbol);
// assert
Assert.That(result, Is.False);
}
[Test]
public void GetSymbolsForBaseAsset_ExistingBaseAsset_Should_ReturnMatchingSymbols()
{
// arrange
var topicId = "ExchangeBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(2));
Assert.That(result.Any(x => x.QuoteAsset == "USDT"), Is.True);
Assert.That(result.Any(x => x.QuoteAsset == "EUR"), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_CaseInsensitive_Should_ReturnMatchingSymbols()
{
// arrange
var topicId = "ExchangeCaseInsensitive";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "btc");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(2));
}
[Test]
public void GetSymbolsForBaseAsset_NonExistingBaseAsset_Should_ReturnEmptyArray()
{
// arrange
var topicId = "ExchangeNoBaseAsset";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "LINK");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void GetSymbolsForBaseAsset_NonExistentTopic_Should_ReturnEmptyArray()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "BTC");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Length, Is.EqualTo(0));
}
[Test]
public void ParseSymbol_ExistingSymbol_Should_ReturnSharedSymbol()
{
// arrange
var topicId = "ExchangeParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "BTCUSDT");
// assert
Assert.That(result, Is.Not.Null);
Assert.That(result.BaseAsset, Is.EqualTo("BTC"));
Assert.That(result.QuoteAsset, Is.EqualTo("USDT"));
Assert.That(result.TradingMode, Is.EqualTo(TradingMode.Spot));
Assert.That(result.SymbolName, Is.EqualTo("BTCUSDT"));
}
[Test]
public void ParseSymbol_NonExistingSymbol_Should_ReturnNull()
{
// arrange
var topicId = "ExchangeNoParse";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, "LINKUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_NullSymbolName_Should_ReturnNull()
{
// arrange
var topicId = "ExchangeNullSymbol";
var symbols = CreateTestSymbols();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.ParseSymbol(topicId, null);
// assert
Assert.That(result, Is.Null);
}
[Test]
public void ParseSymbol_NonExistentTopic_Should_ReturnNull()
{
// arrange
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
// act
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "BTCUSDT");
// assert
Assert.That(result, Is.Null);
}
[Test]
public void MultipleTopics_Should_MaintainSeparateData()
{
// arrange
var topic1 = "Exchange1";
var topic2 = "Exchange2";
var symbols1 = new[]
{
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot)
};
var symbols2 = new[]
{
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot)
};
// act
ExchangeSymbolCache.UpdateSymbolInfo(topic1, symbols1);
ExchangeSymbolCache.UpdateSymbolInfo(topic2, symbols2);
// assert
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "BTCUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "ETHUSDT"), Is.False);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "ETHUSDT"), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "BTCUSDT"), Is.False);
}
[Test]
public void UpdateSymbolInfo_WithDifferentTradingModes_Should_StoreCorrectly()
{
// arrange
var topicId = "ExchangeMixedModes";
var spotSymbols = CreateTestSymbols();
var futuresSymbols = CreateFuturesSymbols();
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
// act
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
// assert
var spotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var futuresSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, spotSymbol), Is.True);
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, futuresSymbol), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_Should_ReturnAllTradingModes()
{
// arrange
var topicId = "ExchangeAllModes";
var spotSymbols = CreateTestSymbols();
var futuresSymbols = CreateFuturesSymbols();
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
// assert
Assert.That(result.Length, Is.GreaterThanOrEqualTo(2));
Assert.That(result.Any(x => x.TradingMode == TradingMode.Spot), Is.True);
Assert.That(result.Any(x => x.TradingMode == TradingMode.PerpetualLinear), Is.True);
}
[Test]
public void GetSymbolsForBaseAsset_WithMultipleMatchingSymbols_Should_ReturnAll()
{
// arrange
var topicId = "ExchangeMultiple";
var symbols = new[]
{
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
new SharedSpotSymbol("ETH", "EUR", "ETHEUR", true, TradingMode.Spot)
};
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
// act
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "ETH");
// assert
Assert.That(result.Length, Is.EqualTo(3));
Assert.That(result.All(x => x.BaseAsset == "ETH"), Is.True);
}
}
}

View File

@ -1,633 +0,0 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SharedQuantityTests
{
[Test]
public void SharedQuantityReference_IsZero_AllNull_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(null, null, null);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantityReference_IsZero_AllZero_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(0, 0, 0);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantityReference_IsZero_BaseAssetSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(1.5m, null, null);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_QuoteAssetSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(null, 100m, null);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_ContractsSet_Should_ReturnFalse()
{
// arrange
var quantity = new SharedOrderQuantity(null, null, 10m);
// act & assert
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedQuantityReference_IsZero_NegativeValue_Should_ReturnTrue()
{
// arrange
var quantity = new SharedOrderQuantity(-1m, 0, 0);
// act & assert
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_DefaultConstructor_Should_SetAllPropertiesToNull()
{
// arrange & act
var quantity = new SharedQuantity();
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_Base_Should_SetBaseAssetQuantity()
{
// arrange
var expectedQuantity = 1.5m;
// act
var quantity = SharedQuantity.Base(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedQuantity));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_Base_WithZero_Should_SetZeroQuantity()
{
// arrange & act
var quantity = SharedQuantity.Base(0m);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(0m));
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_Base_WithLargeValue_Should_SetCorrectly()
{
// arrange
var largeValue = 999999.123456789m;
// act
var quantity = SharedQuantity.Base(largeValue);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(largeValue));
}
[Test]
public void SharedQuantity_Quote_Should_SetQuoteAssetQuantity()
{
// arrange
var expectedQuantity = 100m;
// act
var quantity = SharedQuantity.Quote(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(expectedQuantity));
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_Quote_WithDecimal_Should_PreserveDecimals()
{
// arrange
var expectedQuantity = 50.123456m;
// act
var quantity = SharedQuantity.Quote(expectedQuantity);
// assert
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_Contracts_Should_SetContractQuantity()
{
// arrange
var expectedQuantity = 10m;
// act
var quantity = SharedQuantity.Contracts(expectedQuantity);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_Contracts_WithFractionalValue_Should_SetCorrectly()
{
// arrange
var expectedQuantity = 2.5m;
// act
var quantity = SharedQuantity.Contracts(expectedQuantity);
// assert
Assert.That(quantity.QuantityInContracts, Is.EqualTo(expectedQuantity));
}
[Test]
public void SharedQuantity_BaseFromQuote_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 100m;
var price = 50m;
var expectedBase = 2m; // 100 / 50 = 2
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedBase));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_BaseFromQuote_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var quoteQuantity = 100m;
var price = 3m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, decimalPlaces);
// assert
// 100 / 3 = 33.333... should round to 33.33
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(33.33m));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var quoteQuantity = 100m;
var price = 7m;
var lotSize = 0.1m;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, 8, lotSize);
// assert
// 100 / 7 = 14.285714... should adjust to nearest 0.1 = 14.3
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(14.3m));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithHighPrecision_Should_HandleCorrectly()
{
// arrange
var quoteQuantity = 1000m;
var price = 0.00001m;
var decimalPlaces = 8;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, decimalPlaces);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_QuoteFromBase_Should_CalculateCorrectly()
{
// arrange
var baseQuantity = 2m;
var price = 50m;
var expectedQuote = 100m; // 2 * 50 = 100
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedQuote));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_QuoteFromBase_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var baseQuantity = 1.234567m;
var price = 10m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, decimalPlaces);
// assert
// 1.234567 * 10 = 12.34567 should round to 12.35
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(12.35m));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var baseQuantity = 3.456m;
var price = 10m;
var lotSize = 1m;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, 8, lotSize);
// assert
// 3.456 * 10 = 34.56 should adjust to nearest 1 = 35
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(35m));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithSmallValues_Should_HandleCorrectly()
{
// arrange
var baseQuantity = 0.001m;
var price = 0.1m;
var decimalPlaces = 8;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, decimalPlaces);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(0.0001m));
}
[Test]
public void SharedQuantity_ContractsFromBase_Should_CalculateCorrectly()
{
// arrange
var baseQuantity = 100m;
var contractSize = 10m;
var expectedContracts = 10m; // 100 / 10 = 10
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedContracts));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_ContractsFromBase_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var baseQuantity = 100m;
var contractSize = 3m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize, decimalPlaces);
// assert
// 100 / 3 = 33.333... should round to 33.33
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(33.33m));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var baseQuantity = 100m;
var contractSize = 7m;
var lotSize = 0.5m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize, 8, lotSize);
// assert
// 100 / 7 = 14.285714... should adjust to nearest 0.5 = 14.5
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(14.5m));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithFractionalContract_Should_HandleCorrectly()
{
// arrange
var baseQuantity = 1m;
var contractSize = 0.1m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(10m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 10m;
var price = 50m;
var expectedContracts = 2m; // 1000 / 50 / 10 = 2
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedContracts));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithCustomDecimals_Should_RoundCorrectly()
{
// arrange
var quoteQuantity = 100m;
var contractSize = 3m;
var price = 7m;
var decimalPlaces = 2;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, decimalPlaces);
// assert
// 100 / 7 / 3 = 4.761904... should round to 4.76
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(4.76m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithLotSize_Should_AdjustToLotSize()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 7m;
var price = 13m;
var lotSize = 0.5m;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, 8, lotSize);
// assert
// 1000 / 13 / 7 = 10.989... should adjust to nearest 0.5 = 11.0
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(11.0m));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithComplexValues_Should_CalculateCorrectly()
{
// arrange
var quoteQuantity = 5000m;
var contractSize = 0.01m;
var price = 25000m;
var decimalPlaces = 4;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, decimalPlaces);
// assert
// 5000 / 25000 / 0.01 = 20
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(20m));
}
[Test]
public void SharedOrderQuantity_DefaultConstructor_Should_SetAllPropertiesToNull()
{
// arrange & act
var quantity = new SharedOrderQuantity();
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetBaseAsset()
{
// arrange
var baseAsset = 5m;
// act
var quantity = new SharedOrderQuantity(baseAssetQuantity: baseAsset);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(baseAsset));
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetQuoteAsset()
{
// arrange
var quoteAsset = 100m;
// act
var quantity = new SharedOrderQuantity(quoteAssetQuantity: quoteAsset);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(quantity.QuantityInContracts, Is.Null);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetContracts()
{
// arrange
var contracts = 10m;
// act
var quantity = new SharedOrderQuantity(contractQuantity: contracts);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.EqualTo(contracts));
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetAllValues()
{
// arrange
var baseAsset = 1m;
var quoteAsset = 50m;
var contracts = 5m;
// act
var quantity = new SharedOrderQuantity(baseAsset, quoteAsset, contracts);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(baseAsset));
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(quantity.QuantityInContracts, Is.EqualTo(contracts));
Assert.That(quantity.IsZero, Is.False);
}
[Test]
public void SharedOrderQuantity_ParameterizedConstructor_WithNullValues_Should_HandleCorrectly()
{
// arrange & act
var quantity = new SharedOrderQuantity(null, null, null);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
Assert.That(quantity.QuantityInContracts, Is.Null);
Assert.That(quantity.IsZero, Is.True);
}
[Test]
public void SharedQuantity_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Base(10m);
// act & assert
Assert.That(quantity1, Is.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_RecordEquality_DifferentValues_Should_NotBeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Base(20m);
// act & assert
Assert.That(quantity1, Is.Not.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_RecordEquality_DifferentTypes_Should_NotBeEqual()
{
// arrange
var quantity1 = SharedQuantity.Base(10m);
var quantity2 = SharedQuantity.Quote(10m);
// act & assert
Assert.That(quantity1, Is.Not.EqualTo(quantity2));
}
[Test]
public void SharedOrderQuantity_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var quantity1 = new SharedOrderQuantity(5m, 100m, 2m);
var quantity2 = new SharedOrderQuantity(5m, 100m, 2m);
// act & assert
Assert.That(quantity1, Is.EqualTo(quantity2));
}
[Test]
public void SharedQuantity_BaseFromQuote_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var quoteQuantity = 100m;
var price = 3m;
// act
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price);
// assert
// Default decimalPlaces = 8, default lotSize = 0.00000001
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_QuoteFromBase_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var baseQuantity = 1.234567m;
var price = 10m;
// act
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_ContractsFromBase_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var baseQuantity = 100m;
var contractSize = 3m;
// act
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
[Test]
public void SharedQuantity_ContractsFromQuote_WithDefaultParameters_Should_UseDefaults()
{
// arrange
var quoteQuantity = 1000m;
var contractSize = 10m;
var price = 50m;
// act
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price);
// assert
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
}
}
}

View File

@ -1,400 +0,0 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture()]
public class SharedSymbolTests
{
[Test]
public void SharedSymbol_Constructor_Should_SetAllProperties()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.DeliverTime, Is.Null);
Assert.That(symbol.SymbolName, Is.Null);
}
[Test]
public void SharedSymbol_Constructor_WithDeliveryTime_Should_SetDeliveryTime()
{
// arrange
var tradingMode = TradingMode.DeliveryLinear;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var deliveryTime = new DateTime(2026, 6, 25, 0, 0, 0, DateTimeKind.Utc);
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliveryTime);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.DeliverTime, Is.EqualTo(deliveryTime));
Assert.That(symbol.SymbolName, Is.Null);
}
[Test]
public void SharedSymbol_Constructor_WithNullDeliveryTime_Should_SetToNull()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "ETH";
var quoteAsset = "BTC";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime: null);
// assert
Assert.That(symbol.DeliverTime, Is.Null);
}
[TestCase(TradingMode.Spot)]
[TestCase(TradingMode.PerpetualLinear)]
[TestCase(TradingMode.PerpetualInverse)]
[TestCase(TradingMode.DeliveryLinear)]
[TestCase(TradingMode.DeliveryInverse)]
public void SharedSymbol_Constructor_WithDifferentTradingModes_Should_SetCorrectly(TradingMode tradingMode)
{
// arrange
var baseAsset = "BTC";
var quoteAsset = "USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_Should_SetSymbolName()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var symbolName = "BTC-USDT";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
Assert.That(symbol.SymbolName, Is.EqualTo(symbolName));
Assert.That(symbol.DeliverTime, Is.Null);
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_WithCustomFormat_Should_SetCorrectly()
{
// arrange
var tradingMode = TradingMode.PerpetualLinear;
var baseAsset = "ETH";
var quoteAsset = "USDT";
var symbolName = "ETHUSDT-PERP";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.SymbolName, Is.EqualTo(symbolName));
}
[Test]
public void SharedSymbol_ConstructorWithSymbolName_WithEmptyString_Should_SetEmptyString()
{
// arrange
var tradingMode = TradingMode.Spot;
var baseAsset = "BTC";
var quoteAsset = "USDT";
var symbolName = "";
// act
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
// assert
Assert.That(symbol.SymbolName, Is.EqualTo(""));
}
[Test]
public void GetSymbol_WithSymbolNameSet_Should_ReturnSymbolName()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "CUSTOM-BTC-USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("CUSTOM-BTC-USDT"));
}
[Test]
public void GetSymbol_WithSymbolNameNull_Should_UseFormatFunction()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}/{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTC/USDT"));
}
[Test]
public void GetSymbol_WithComplexFormatFunction_Should_ApplyCorrectly()
{
// arrange
var symbol = new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => t == TradingMode.PerpetualLinear ? $"{b}{q}-PERP" : $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("ETHUSDT-PERP"));
}
[Test]
public void GetSymbol_WithDeliveryTime_Should_PassDeliveryTimeToFormatter()
{
// arrange
var deliveryTime = new DateTime(2026, 6, 25);
var symbol = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime);
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => d.HasValue ? $"{b}{q}_{d.Value:yyyyMMdd}" : $"{b}{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTCUSDT_20260625"));
}
[Test]
public void GetSymbol_WithTradingMode_Should_PassTradingModeToFormatter()
{
// arrange
var symbol = new SharedSymbol(TradingMode.PerpetualInverse, "BTC", "USD");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) =>
{
return t switch
{
TradingMode.Spot => $"{b}{q}",
TradingMode.PerpetualLinear => $"{b}{q}-PERP",
TradingMode.PerpetualInverse => $"{b}{q}I-PERP",
_ => $"{b}{q}"
};
});
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTCUSDI-PERP"));
}
[Test]
public void GetSymbol_WithEmptySymbolName_Should_UseFormatFunction()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}-{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo("BTC-USDT"));
}
[Test]
public void GetSymbol_WithWhitespaceSymbolName_Should_ReturnWhitespace()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", " ");
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
(b, q, t, d) => $"{b}-{q}");
// act
var result = symbol.GetSymbol(formatFunc);
// assert
Assert.That(result, Is.EqualTo(" "));
}
[Test]
public void SharedSymbol_RecordEquality_SameValues_Should_BeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentBaseAsset_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentQuoteAsset_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "EUR");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentTradingMode_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var symbol2 = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentDeliveryTime_Should_NotBeEqual()
{
// arrange
var deliveryTime1 = new DateTime(2026, 6, 25);
var deliveryTime2 = new DateTime(2026, 9, 25);
var symbol1 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime1);
var symbol2 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime2);
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_DifferentSymbolName_Should_NotBeEqual()
{
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTCUSDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTC-USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_OneWithSymbolNameOneWithout_Should_NotBeEqual()
{
// NOTE; although this should probably be equal it's considered not because the SymbolName property isn't equal
// Overridding equality to ignore SymbolName would be possible but would break the default record equality behavior and cause confusion
// arrange
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTCUSDT");
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act & assert
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_RecordEquality_WithAllPropertiesSet_Should_BeEqual()
{
// arrange
var deliveryTime = new DateTime(2026, 6, 25);
var symbol1 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime)
{
SymbolName = "BTCUSDT-0625"
};
var symbol2 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime)
{
SymbolName = "BTCUSDT-0625"
};
// act & assert
Assert.That(symbol1, Is.EqualTo(symbol2));
}
[Test]
public void SharedSymbol_Properties_Should_BeSettable()
{
// arrange
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// act
symbol.BaseAsset = "ETH";
symbol.QuoteAsset = "EUR";
symbol.TradingMode = TradingMode.PerpetualLinear;
symbol.SymbolName = "CUSTOM";
symbol.DeliverTime = DateTime.UtcNow;
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo("ETH"));
Assert.That(symbol.QuoteAsset, Is.EqualTo("EUR"));
Assert.That(symbol.TradingMode, Is.EqualTo(TradingMode.PerpetualLinear));
Assert.That(symbol.SymbolName, Is.EqualTo("CUSTOM"));
Assert.That(symbol.DeliverTime, Is.Not.Null);
}
[Test]
public void SharedSymbol_WithSpecialCharactersInAssets_Should_HandleCorrectly()
{
// arrange
var baseAsset = "BTC-123";
var quoteAsset = "USDT_2.0";
// act
var symbol = new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset);
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
}
[Test]
public void SharedSymbol_WithLongAssetNames_Should_HandleCorrectly()
{
// arrange
var baseAsset = "VERYLONGASSETNAMEFORTESTING";
var quoteAsset = "ANOTHERVERYLONGASSETNAME";
// act
var symbol = new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset);
// assert
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
}
}
}

View File

@ -304,9 +304,55 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
}
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
if (!subscribeResult)
return new CallResult<UpdateSubscription>(subscribeResult.Error!);
void HandleSubscriptionComplete(bool success, object? response)
{
if (!success)
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);
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));

View File

@ -168,7 +168,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (!_unknownValuesWarned.Contains(stringValue))
{
_unknownValuesWarned.Add(stringValue!);
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");
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");
}
}
@ -246,12 +246,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{
// If no explicit mapping is found try to parse string
result = (T)Enum.Parse(objectType, value, true);
if (!Enum.IsDefined(objectType, result))
{
result = default;
return false;
}
return true;
}
catch (Exception)

View File

@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId>
<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>
<PackageVersion>10.5.4</PackageVersion>
<AssemblyVersion>10.5.4</AssemblyVersion>
<FileVersion>10.5.4</FileVersion>
<PackageVersion>10.4.1</PackageVersion>
<AssemblyVersion>10.4.1</AssemblyVersion>
<FileVersion>10.4.1</FileVersion>
<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>
<RepositoryType>git</RepositoryType>

View File

@ -37,7 +37,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, int, string, Exception?> _sendingPeriodic;
private static readonly Action<ILogger, int, string, string, Exception?> _periodicSendFailed;
private static readonly Action<ILogger, int, int, string, Exception?> _sendingData;
private static readonly Action<ILogger, int, string, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
private static readonly Action<ILogger, int, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
private static readonly Action<ILogger, int, int, int, Exception?> _sendingByteData;
static SocketConnectionLoggingExtension()
@ -177,10 +177,10 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(2028, "SendingData"),
"[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}");
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string, string>(
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
LogLevel.Warning,
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
"[Sckt {SocketId}] received message not matched to any listener. TypeIdentifier: {TypeIdentifier}, ListenId: {ListenId}, current listeners: [{ListenIds}]");
"[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: [{ListenIds}]");
_failedToParse = LoggerMessage.Define<int, string>(
LogLevel.Warning,
@ -326,9 +326,9 @@ namespace CryptoExchange.Net.Logging.Extensions
_sendingData(logger, socketId, requestId, data, null);
}
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string typeIdentifier, string listenId, string listenIds)
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string listenId, string listenIds)
{
_receivedMessageNotMatchedToAnyListener(logger, socketId, typeIdentifier, listenId, listenIds, null);
_receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null);
}
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
@ -100,9 +99,6 @@ namespace CryptoExchange.Net.SharedApis
if (val == null)
return default;
if (val.Value is T typeVal)
return typeVal;
try
{
Type t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);

View File

@ -87,8 +87,7 @@ namespace CryptoExchange.Net.Sockets.Default
get
{
UpdateReceivedMessages();
var seconds = (_lastBytesReceivedUpdate - _prevSlotBytesReceivedUpdate).TotalSeconds;
return seconds > 0 ? Math.Round(_prevSlotBytesReceived / seconds / 1000) : 0;
return Math.Round(_prevSlotBytesReceived * (_lastBytesReceivedUpdate - _prevSlotBytesReceivedUpdate).TotalSeconds / 1000);
}
}

View File

@ -633,37 +633,22 @@ namespace CryptoExchange.Net.Sockets.Default
if (route.TypeIdentifier != typeIdentifier)
continue;
// Forward message rules:
// | Message Topic | Route Topic Filter | Topics Match | Forward | Description
// | N | N | - | Y | No topic filter applied
// | N | Y | - | N | Route only listens to specific topic
// | Y | N | - | Y | Route listens to all message regardless of topic
// | Y | Y | Y | Y | Route listens to specific message topic
// | Y | Y | N | N | Route listens to different topic
if (topicFilter == null)
if (topicFilter == null
|| route.TopicFilter == null
|| route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal))
{
if (route.TopicFilter != null)
// No topic on message, but route is filtering on topic
processed = true;
if (isQuery && query!.Completed)
continue;
processor.Handle(this, receiveTime, originalData, result, route);
if (isQuery && !route.MultipleReaders)
{
complete = true;
break;
}
}
else
{
if (route.TopicFilter != null && !route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal))
// Message has a topic, and the route has a filter for another topic
continue;
}
processed = true;
if (isQuery && query!.Completed)
continue;
processor.Handle(this, receiveTime, originalData, result, route);
if (isQuery && !route.MultipleReaders)
{
complete = true;
break;
}
}
if (complete)
@ -673,16 +658,10 @@ namespace CryptoExchange.Net.Sockets.Default
if (!processed)
{
if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data))
lock (_listenersLock)
{
lock (_listenersLock)
{
_logger.ReceivedMessageNotMatchedToAnyListener(
SocketId,
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]")))));
}
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, topicFilter!,
string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]")))));
}
}
}
@ -951,7 +930,7 @@ namespace CryptoExchange.Net.Sockets.Default
return SendStringAsync(requestId, str, weight);
str = stringSerializer.Serialize(obj);
return SendStringAsync(requestId, str, weight);
return SendAsync(requestId, str, weight);
}
throw new Exception("Unknown serializer when sending message");
@ -1085,8 +1064,40 @@ namespace CryptoExchange.Net.Sockets.Default
var taskList = new List<Task<CallResult>>();
foreach (var subscription in subList)
{
var subscribeTask = TrySubscribeAsync(subscription, false, default);
taskList.Add(subscribeTask);
subscription.ConnectionInvocations = 0;
if (!subscription.Active)
// 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);
@ -1103,61 +1114,6 @@ namespace CryptoExchange.Net.Sockets.Default
return CallResult.SuccessResult;
}
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)
{
var unsubscribeRequest = subscription.CreateUnsubscriptionQuery(this);

View File

@ -174,14 +174,6 @@ namespace CryptoExchange.Net.Sockets.Default
{
ConnectionInvocations++;
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);
}

View File

@ -127,8 +127,8 @@ namespace CryptoExchange.Net.Sockets
}
else
{
Result = CallResult.SuccessResult;
Completed = true;
Result = CallResult.SuccessResult;
_event.Set();
}
}
@ -216,12 +216,12 @@ namespace CryptoExchange.Net.Sockets
if (Completed)
return;
Completed = true;
if (TimeoutBehavior == TimeoutBehavior.Fail)
Result = new CallResult<THandlerResponse>(new TimeoutError());
else
Result = new CallResult<THandlerResponse>(default, null, default);
Completed = true;
_event.Set();
OnComplete?.Invoke();
}
@ -234,7 +234,6 @@ namespace CryptoExchange.Net.Sockets
Result = new CallResult<THandlerResponse>(error);
Completed = true;
_event.Set();
OnComplete?.Invoke();
}

View File

@ -16,6 +16,13 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
/// </summary>
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>
/// On connection status change. Might trigger multiple times with the same status depending on the underlying subscriptions.
/// </summary>

View File

@ -2,7 +2,6 @@
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
@ -27,13 +26,6 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
/// </summary>
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>
/// Balances tracker
/// </summary>

View File

@ -2,7 +2,6 @@
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
@ -27,13 +26,6 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
/// </summary>
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>
/// Balances tracker
/// </summary>

View File

@ -22,13 +22,12 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
/// </summary>
public BalanceTracker(
ILogger logger,
UserDataSymbolTracker symbolTracker,
IBalanceRestClient restClient,
IBalanceSocketClient? socketClient,
SharedAccountType accountType,
TrackerItemConfig config,
ExchangeParameters? exchangeParameters = null
) : base(logger, symbolTracker, UserDataType.Balances, restClient.Exchange, config)
) : base(logger, UserDataType.Balances, restClient.Exchange, config, false, null)
{
if (_socketClient == null)
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };

View File

@ -19,7 +19,6 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
private readonly IFuturesOrderSocketClient? _socketClient;
private readonly ExchangeParameters? _exchangeParameters;
private readonly bool _requiresSymbolParameterOpenOrders;
private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new();
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
@ -28,14 +27,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
/// </summary>
public FuturesOrderTracker(
ILogger logger,
UserDataSymbolTracker symbolTracker,
IFuturesOrderRestClient restClient,
IFuturesOrderSocketClient? socketClient,
TrackerItemConfig config,
IEnumerable<SharedSymbol> symbols,
bool onlyTrackProvidedSymbols,
ExchangeParameters? exchangeParameters = null
) : base(logger, symbolTracker, UserDataType.Orders, restClient.Exchange, config)
) : base(logger, UserDataType.Orders, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
{
if (_socketClient == null)
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
@ -131,14 +129,6 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
// status changed from open to not open
return true;
if (existingItem.Status != SharedOrderStatus.Open
&& updateItem.Status != SharedOrderStatus.Open
&& existingItem.Status != updateItem.Status)
{
_logger.LogWarning("Invalid order update detected for order {OrderId}; current status: {OldStatus}, new status: {NewStatus}", existingItem.OrderId, existingItem.Status, updateItem.Status);
return false;
}
if (existingItem.Status != SharedOrderStatus.Open && updateItem.Status == SharedOrderStatus.Open)
// status changed from not open to open; stale
return false;
@ -235,7 +225,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
}
else
{
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
foreach (var symbol in _symbols.ToList())
{
var openOrdersResult = await _restClient.GetOpenFuturesOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
if (!openOrdersResult.Success)
@ -253,30 +243,11 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
}
}
}
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))
foreach (var symbol in _symbols.ToList())
{
if (openOrders.Any(x => x.OrderId == order.OrderId))
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 fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
var updatedPollTime = DateTime.UtcNow;
var closedOrdersResult = await _restClient.GetClosedFuturesOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
if (!closedOrdersResult.Success)
{
@ -288,26 +259,22 @@ 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
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
).ToArray();
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
var openOrdersNotReturned = Values.Where(x =>
// Orders for the same symbol
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset
// With no filled value
&& 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)
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset // Orders for the same symbol
&& x.QuantityFilled?.IsZero == true // With no filled value
&& !openOrders.Any(r => r.OrderId == x.OrderId) // Not returned in open orders
&& !relevantOrders.Any(r => r.OrderId == x.OrderId) // Not return in closed orders
).ToList();
var additionalUpdates = new List<SharedFuturesOrder>();
@ -325,64 +292,7 @@ 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";
}
if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(1))
{
// Set it to at least a seconds in the past to prevent issues
fromTime = DateTime.UtcNow.AddSeconds(-1);
}
_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;
}
}
}

View File

@ -26,14 +26,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
/// </summary>
public FuturesUserTradeTracker(
ILogger logger,
UserDataSymbolTracker symbolTracker,
IFuturesOrderRestClient restClient,
IUserTradeSocketClient? socketClient,
TrackerItemConfig config,
IEnumerable<SharedSymbol> symbols,
bool onlyTrackProvidedSymbols,
ExchangeParameters? exchangeParameters = null
) : base(logger, symbolTracker, UserDataType.Trades, restClient.Exchange, config)
) : base(logger, UserDataType.Trades, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
{
if (_socketClient == null)
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
@ -56,10 +55,10 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
protected override async Task<bool> DoPollAsync()
{
var anyError = false;
var fromTimeTrades = GetTradesRequestStartTime();
var updatedPollTime = DateTime.UtcNow;
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
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)
{
@ -81,53 +80,9 @@ 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";
}
var now = DateTime.UtcNow;
if (now - fromTime < TimeSpan.FromSeconds(1))
{
// Set it to at least a seconds in the past to prevent issues
fromTime = now.AddSeconds(-1);
}
_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 />
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
{

View File

@ -29,7 +29,6 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
/// </summary>
public PositionTracker(
ILogger logger,
UserDataSymbolTracker symbolTracker,
IFuturesOrderRestClient restClient,
IPositionSocketClient? socketClient,
TrackerItemConfig config,
@ -37,7 +36,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
bool onlyTrackProvidedSymbols,
bool websocketPositionUpdatesAreFullSnapshots,
ExchangeParameters? exchangeParameters = null
) : base(logger, symbolTracker, UserDataType.Positions, restClient.Exchange, config)
) : base(logger, UserDataType.Positions, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
{
if (_socketClient == null)
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
@ -119,9 +118,9 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
{
toRemove ??= new List<SharedPosition>();
toRemove.Add(item);
_logger.LogWarning("Ignoring {DataType} update for {Key}, no SharedSymbol set", DataType, GetKey(item));
}
else if (!_symbolTracker.ShouldProcess(symbolModel.SharedSymbol))
else if (_onlyTrackProvidedSymbols
&& !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
{
toRemove ??= new List<SharedPosition>();
toRemove.Add(item);
@ -132,7 +131,8 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
if (toRemove != null)
@event = @event.Except(toRemove).ToArray();
_symbolTracker.UpdateTrackedSymbols(@event.Where(x => x.PositionSize > 0).OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
if (!_onlyTrackProvidedSymbols)
UpdateSymbolsList(@event.Where(x => x.PositionSize > 0).OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
// Update local store

View File

@ -19,7 +19,6 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
private readonly ISpotOrderSocketClient? _socketClient;
private readonly ExchangeParameters? _exchangeParameters;
private readonly bool _requiresSymbolParameterOpenOrders;
private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new();
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
@ -28,14 +27,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
/// </summary>
public SpotOrderTracker(
ILogger logger,
UserDataSymbolTracker symbolTracker,
ISpotOrderRestClient restClient,
ISpotOrderSocketClient? socketClient,
TrackerItemConfig config,
IEnumerable<SharedSymbol> symbols,
bool onlyTrackProvidedSymbols,
ExchangeParameters? exchangeParameters = null
) : base(logger, symbolTracker, UserDataType.Orders, restClient.Exchange, config)
) : base(logger, UserDataType.Orders, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
{
if (_socketClient == null)
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
@ -142,14 +140,6 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
// status changed from open to not open
return true;
if (existingItem.Status != SharedOrderStatus.Open
&& updateItem.Status != SharedOrderStatus.Open
&& existingItem.Status != updateItem.Status)
{
_logger.LogWarning("Invalid order update detected for order {OrderId}; current status: {OldStatus}, new status: {NewStatus}", existingItem.OrderId, existingItem.Status, updateItem.Status);
return false;
}
if (existingItem.Status != SharedOrderStatus.Open && updateItem.Status == SharedOrderStatus.Open)
// status changed from not open to open; stale
return false;
@ -246,7 +236,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
}
else
{
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
foreach (var symbol in _symbols.ToList())
{
var openOrdersResult = await _restClient.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
if (!openOrdersResult.Success)
@ -268,27 +258,10 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
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))
foreach (var symbol in _symbols.ToList())
{
if (openOrders.Any(x => x.OrderId == order.OrderId))
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 fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
var updatedPollTime = DateTime.UtcNow;
var closedOrdersResult = await _restClient.GetClosedSpotOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
if (!closedOrdersResult.Success)
{
@ -300,26 +273,22 @@ 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
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
).ToArray();
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
var openOrdersNotReturned = Values.Where(x =>
// Orders for the same symbol
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset
// With no filled value
&& 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)
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset // Orders for the same symbol
&& x.QuantityFilled?.IsZero == true // With no filled value
&& !openOrders.Any(r => r.OrderId == x.OrderId) // Not returned in open orders
&& !relevantOrders.Any(r => r.OrderId == x.OrderId) // Not return in closed orders
).ToList();
var additionalUpdates = new List<SharedSpotOrder>();
@ -337,64 +306,7 @@ 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";
}
if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(1))
{
// Set it to at least a seconds in the past to prevent issues
fromTime = DateTime.UtcNow.AddSeconds(-1);
}
_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;
}
}
}

View File

@ -26,14 +26,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
/// </summary>
public SpotUserTradeTracker(
ILogger logger,
UserDataSymbolTracker symbolTracker,
ISpotOrderRestClient restClient,
IUserTradeSocketClient? socketClient,
TrackerItemConfig config,
IEnumerable<SharedSymbol> symbols,
bool onlyTrackProvidedSymbols,
ExchangeParameters? exchangeParameters = null
) : base(logger, symbolTracker, UserDataType.Trades, restClient.Exchange, config)
) : base(logger, UserDataType.Trades, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
{
if (_socketClient == null)
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
@ -56,10 +55,10 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
protected override async Task<bool> DoPollAsync()
{
var anyError = false;
var fromTimeTrades = GetTradesRequestStartTime();
var updatedPollTime = DateTime.UtcNow;
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
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)
{
@ -71,6 +70,8 @@ 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();
@ -79,52 +80,9 @@ 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";
}
if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(1))
{
// Set it to at least a seconds in the past to prevent issues
fromTime = DateTime.UtcNow.AddSeconds(-1);
}
_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 />
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
{

View File

@ -203,14 +203,21 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
/// </summary>
protected ConcurrentDictionary<string, T> _store = new ConcurrentDictionary<string, T>(StringComparer.InvariantCultureIgnoreCase);
/// <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
/// </summary>
protected bool _isSymbolModel;
/// <summary>
/// Symbol tracker
/// </summary>
protected readonly UserDataSymbolTracker _symbolTracker;
/// <inheritdoc />
public T[] Values
@ -233,23 +240,22 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
/// <inheritdoc />
public event Func<UserDataUpdate<T[]>, Task>? OnUpdate;
/// <inheritdoc />
public IEnumerable<SharedSymbol> TrackedSymbols => _symbols;
/// <summary>
/// ctor
/// </summary>
public UserDataItemTracker(
ILogger logger,
UserDataSymbolTracker symbolTracker,
UserDataType dataType,
string exchange,
TrackerItemConfig config) : base(logger, dataType, exchange)
public UserDataItemTracker(ILogger logger, UserDataType dataType, string exchange, TrackerItemConfig config, bool onlyTrackProvidedSymbols, IEnumerable<SharedSymbol>? symbols) : 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;
}
/// <summary>
@ -328,7 +334,26 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
/// Get the age of an item
/// </summary>
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>
/// Handle an update
/// </summary>
@ -347,9 +372,9 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
{
toRemove ??= new List<T>();
toRemove.Add(item);
_logger.LogWarning("Ignoring {DataType} update for {Key}, no SharedSymbol set", DataType, GetKey(item));
}
else if (!_symbolTracker.ShouldProcess(symbolModel.SharedSymbol))
else if (_onlyTrackProvidedSymbols
&& !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
{
toRemove ??= new List<T>();
toRemove.Add(item);
@ -360,7 +385,8 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
if (toRemove != null)
@event = @event.Except(toRemove).ToArray();
_symbolTracker.UpdateTrackedSymbols(@event.OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
if (!_onlyTrackProvidedSymbols)
UpdateSymbolsList(@event.OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
}
// Update local store

View File

@ -1,60 +0,0 @@
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
{
public class UserDataSymbolTracker
{
private readonly ILogger _logger;
private readonly List<SharedSymbol> _trackedSymbols;
private readonly bool _onlyTrackProvidedSymbols;
private readonly object _symbolLock = new object();
public UserDataSymbolTracker(ILogger logger, UserDataTrackerConfig config)
{
_logger = logger;
_trackedSymbols = config.TrackedSymbols?.ToList() ?? [];
_onlyTrackProvidedSymbols = config.OnlyTrackProvidedSymbols;
}
public IEnumerable<SharedSymbol> GetTrackedSymbols()
{
lock (_symbolLock)
return _trackedSymbols.ToList();
}
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>
/// <param name="symbols"></param>
public void UpdateTrackedSymbols(IEnumerable<SharedSymbol> symbols)
{
if (_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);
}
}
}
}
}
}

View File

@ -1,12 +1,10 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
using CryptoExchange.Net.Trackers.UserData.Objects;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Trackers.UserData
@ -24,21 +22,11 @@ namespace CryptoExchange.Net.Trackers.UserData
/// Listen key to use for subscriptions
/// </summary>
protected string? _listenKey;
/// <summary>
/// Cts
/// </summary>
protected CancellationTokenSource? _cts;
/// <summary>
/// List of data trackers
/// </summary>
protected abstract UserDataItemTracker[] DataTrackers { get; }
/// <summary>
/// Symbol tracker
/// </summary>
protected internal UserDataSymbolTracker SymbolTracker { get; }
/// <inheritdoc />
public string? UserIdentifier { get; }
@ -57,11 +45,6 @@ namespace CryptoExchange.Net.Trackers.UserData
/// </summary>
public bool Connected => DataTrackers.All(x => x.Connected);
/// <summary>
/// Currently tracked symbols
/// </summary>
public IEnumerable<SharedSymbol> TrackedSymbols => SymbolTracker.GetTrackedSymbols();
/// <summary>
/// ctor
/// </summary>
@ -76,7 +59,6 @@ namespace CryptoExchange.Net.Trackers.UserData
_logger = logger;
SymbolTracker = new UserDataSymbolTracker(logger, config);
Exchange = exchange;
UserIdentifier = userIdentifier;
}
@ -86,8 +68,6 @@ namespace CryptoExchange.Net.Trackers.UserData
/// </summary>
public async Task<CallResult> StartAsync()
{
_cts = new CancellationTokenSource();
foreach(var tracker in DataTrackers)
tracker.OnConnectedChange += (x) => OnConnectedChange?.Invoke(tracker.DataType, x);
@ -120,21 +100,12 @@ namespace CryptoExchange.Net.Trackers.UserData
public async Task StopAsync()
{
_logger.LogDebug("Stopping UserDataTracker");
_cts?.Cancel();
var tasks = new List<Task>();
foreach (var dataTracker in DataTrackers)
tasks.Add(dataTracker.StopAsync());
await DoStopAsync().ConfigureAwait(false);
await Task.WhenAll(tasks).ConfigureAwait(false);
_logger.LogDebug("Stopped UserDataTracker");
}
/// <summary>
/// Stop implementation
/// </summary>
/// <returns></returns>
protected virtual Task DoStopAsync() => Task.CompletedTask;
}
}

View File

@ -20,8 +20,6 @@ namespace CryptoExchange.Net.Trackers.UserData
private readonly IFuturesSymbolRestClient _symbolClient;
private readonly IListenKeyRestClient? _listenKeyClient;
private readonly ExchangeParameters? _exchangeParameters;
private readonly TradingMode _tradingMode;
private Task? _lkKeepAliveTask;
/// <inheritdoc />
protected override UserDataItemTracker[] DataTrackers { get; }
@ -70,28 +68,24 @@ namespace CryptoExchange.Net.Trackers.UserData
_listenKeyClient = listenKeyRestClient;
_exchangeParameters = exchangeParameters;
_tradingMode = accountType == SharedAccountType.PerpetualInverseFutures ? TradingMode.PerpetualInverse :
accountType == SharedAccountType.DeliveryLinearFutures ? TradingMode.DeliveryLinear :
accountType == SharedAccountType.DeliveryInverseFutures ? TradingMode.DeliveryInverse :
TradingMode.PerpetualLinear;
var trackers = new List<UserDataItemTracker>();
var balanceTracker = new BalanceTracker(logger, SymbolTracker, balanceRestClient, balanceSocketClient, accountType ?? SharedAccountType.PerpetualLinearFutures, config.BalancesConfig, exchangeParameters);
var balanceAccountType = accountType ?? SharedAccountType.PerpetualLinearFutures;
var balanceTracker = new BalanceTracker(logger, balanceRestClient, balanceSocketClient, balanceAccountType, config.BalancesConfig, exchangeParameters);
Balances = balanceTracker;
trackers.Add(balanceTracker);
var orderTracker = new FuturesOrderTracker(logger, SymbolTracker, futuresOrderRestClient, futuresOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
var orderTracker = new FuturesOrderTracker(logger, futuresOrderRestClient, futuresOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
Orders = orderTracker;
trackers.Add(orderTracker);
var positionTracker = new PositionTracker(logger, SymbolTracker, futuresOrderRestClient, positionSocketClient, config.PositionConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, WebsocketPositionUpdatesAreFullSnapshots, exchangeParameters);
var positionTracker = new PositionTracker(logger, futuresOrderRestClient, positionSocketClient, config.PositionConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, WebsocketPositionUpdatesAreFullSnapshots, exchangeParameters);
Positions = positionTracker;
trackers.Add(positionTracker);
if (config.TrackTrades)
{
var tradeTracker = new FuturesUserTradeTracker(logger, SymbolTracker, futuresOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
var tradeTracker = new FuturesUserTradeTracker(logger, futuresOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
Trades = tradeTracker;
trackers.Add(tradeTracker);
@ -105,7 +99,7 @@ namespace CryptoExchange.Net.Trackers.UserData
/// <inheritdoc />
protected override async Task<CallResult> DoStartAsync()
{
var symbolResult = await _symbolClient.GetFuturesSymbolsAsync(new GetSymbolsRequest(_tradingMode, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
var symbolResult = await _symbolClient.GetFuturesSymbolsAsync(new GetSymbolsRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
if (!symbolResult)
{
_logger.LogWarning("Failed to start UserFuturesDataTracker; symbols request failed: {Error}", symbolResult.Error);
@ -114,45 +108,17 @@ namespace CryptoExchange.Net.Trackers.UserData
if (_listenKeyClient != null)
{
var lkResult = await _listenKeyClient.StartListenKeyAsync(new StartListenKeyRequest(_tradingMode, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
var lkResult = await _listenKeyClient.StartListenKeyAsync(new StartListenKeyRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
if (!lkResult)
{
_logger.LogWarning("Failed to start UserFuturesDataTracker; listen key request failed: {Error}", lkResult.Error);
return lkResult;
}
_lkKeepAliveTask = KeepAliveListenKeyAsync();
_listenKey = lkResult.Data;
}
return CallResult.SuccessResult;
}
/// <inheritdoc />
protected override async Task DoStopAsync()
{
if (_lkKeepAliveTask != null)
await _lkKeepAliveTask.ConfigureAwait(false);
}
private async Task KeepAliveListenKeyAsync()
{
var interval = TimeSpan.FromMinutes(30);
while (!_cts!.IsCancellationRequested)
{
try { await Task.Delay(interval, _cts.Token).ConfigureAwait(false); } catch (Exception)
{
break;
}
var result = await _listenKeyClient!.KeepAliveListenKeyAsync(new KeepAliveListenKeyRequest(_listenKey!, _tradingMode)).ConfigureAwait(false);
if (!result)
_logger.LogWarning("Listen key keep alive failed: " + result.Error);
// If failed shorten the delay to allow a couple more retries
interval = result ? TimeSpan.FromMinutes(30) : TimeSpan.FromMinutes(5);
}
}
}
}

View File

@ -7,7 +7,6 @@ using System.Linq;
using CryptoExchange.Net.Trackers.UserData.Interfaces;
using CryptoExchange.Net.Trackers.UserData.Objects;
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
using System;
namespace CryptoExchange.Net.Trackers.UserData
{
@ -19,7 +18,6 @@ namespace CryptoExchange.Net.Trackers.UserData
private readonly ISpotSymbolRestClient _symbolClient;
private readonly IListenKeyRestClient? _listenKeyClient;
private readonly ExchangeParameters? _exchangeParameters;
private Task? _lkKeepAliveTask;
/// <inheritdoc />
protected override UserDataItemTracker[] DataTrackers { get; }
@ -53,17 +51,17 @@ namespace CryptoExchange.Net.Trackers.UserData
var trackers = new List<UserDataItemTracker>();
var balanceTracker = new BalanceTracker(logger, SymbolTracker, balanceRestClient, balanceSocketClient, SharedAccountType.Spot, config.BalancesConfig, exchangeParameters);
var balanceTracker = new BalanceTracker(logger, balanceRestClient, balanceSocketClient, SharedAccountType.Spot, config.BalancesConfig, exchangeParameters);
Balances = balanceTracker;
trackers.Add(balanceTracker);
var orderTracker = new SpotOrderTracker(logger, SymbolTracker, spotOrderRestClient, spotOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
var orderTracker = new SpotOrderTracker(logger, spotOrderRestClient, spotOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
Orders = orderTracker;
trackers.Add(orderTracker);
if (config.TrackTrades)
{
var tradeTracker = new SpotUserTradeTracker(logger, SymbolTracker, spotOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
var tradeTracker = new SpotUserTradeTracker(logger, spotOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
Trades = tradeTracker;
trackers.Add(tradeTracker);
@ -93,39 +91,10 @@ namespace CryptoExchange.Net.Trackers.UserData
return lkResult;
}
_lkKeepAliveTask = KeepAliveListenKeyAsync();
_listenKey = lkResult.Data;
}
return CallResult.SuccessResult;
}
/// <inheritdoc />
protected override async Task DoStopAsync()
{
if (_lkKeepAliveTask != null)
await _lkKeepAliveTask.ConfigureAwait(false);
}
private async Task KeepAliveListenKeyAsync()
{
var interval = TimeSpan.FromMinutes(30);
while (!_cts!.IsCancellationRequested)
{
try { await Task.Delay(interval, _cts.Token).ConfigureAwait(false); }
catch (Exception)
{
break;
}
var result = await _listenKeyClient!.KeepAliveListenKeyAsync(new KeepAliveListenKeyRequest(_listenKey!, TradingMode.Spot)).ConfigureAwait(false);
if (!result)
_logger.LogWarning("Listen key keep alive failed: " + result.Error);
// If failed shorten the delay to allow a couple more retries
interval = result ? TimeSpan.FromMinutes(30) : TimeSpan.FromMinutes(5);
}
}
}
}

View File

@ -5,32 +5,32 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="12.5.0" />
<PackageReference Include="Bitfinex.Net" Version="10.6.0" />
<PackageReference Include="BitMart.Net" Version="3.5.0" />
<PackageReference Include="BloFin.Net" Version="2.5.0" />
<PackageReference Include="Bybit.Net" Version="6.5.0" />
<PackageReference Include="CoinEx.Net" Version="10.5.0" />
<PackageReference Include="CoinW.Net" Version="2.5.0" />
<PackageReference Include="CryptoCom.Net" Version="3.5.0" />
<PackageReference Include="DeepCoin.Net" Version="3.5.0" />
<PackageReference Include="GateIo.Net" Version="3.5.0" />
<PackageReference Include="HyperLiquid.Net" Version="3.7.0" />
<PackageReference Include="JK.BingX.Net" Version="3.5.0" />
<PackageReference Include="JK.Bitget.Net" Version="3.5.0" />
<PackageReference Include="JK.Mexc.Net" Version="4.5.0" />
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
<PackageReference Include="Jkorf.Aster.Net" Version="2.5.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="3.5.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="3.5.1" />
<PackageReference Include="JKorf.HTX.Net" Version="8.5.0" />
<PackageReference Include="JKorf.Upbit.Net" Version="2.5.0" />
<PackageReference Include="KrakenExchange.Net" Version="7.5.0" />
<PackageReference Include="Kucoin.Net" Version="8.5.0" />
<PackageReference Include="Binance.Net" Version="12.1.0" />
<PackageReference Include="Bitfinex.Net" Version="10.2.0" />
<PackageReference Include="BitMart.Net" Version="3.1.0" />
<PackageReference Include="BloFin.Net" Version="2.1.1" />
<PackageReference Include="Bybit.Net" Version="6.1.0" />
<PackageReference Include="CoinEx.Net" Version="10.1.0" />
<PackageReference Include="CoinW.Net" Version="2.1.1" />
<PackageReference Include="CryptoCom.Net" Version="3.1.0" />
<PackageReference Include="DeepCoin.Net" Version="3.1.0" />
<PackageReference Include="GateIo.Net" Version="3.1.0" />
<PackageReference Include="HyperLiquid.Net" Version="3.2.0" />
<PackageReference Include="JK.BingX.Net" Version="3.1.0" />
<PackageReference Include="JK.Bitget.Net" Version="3.1.0" />
<PackageReference Include="JK.Mexc.Net" Version="4.1.0" />
<PackageReference Include="JK.OKX.Net" Version="4.1.0" />
<PackageReference Include="Jkorf.Aster.Net" Version="2.1.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="3.1.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="3.1.0" />
<PackageReference Include="JKorf.HTX.Net" Version="8.1.0" />
<PackageReference Include="JKorf.Upbit.Net" Version="2.1.0" />
<PackageReference Include="KrakenExchange.Net" Version="7.1.0" />
<PackageReference Include="Kucoin.Net" Version="8.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Toobit.Net" Version="3.5.0" />
<PackageReference Include="WhiteBit.Net" Version="3.5.0" />
<PackageReference Include="XT.Net" Version="3.5.0" />
<PackageReference Include="Toobit.Net" Version="2.1.0" />
<PackageReference Include="WhiteBit.Net" Version="3.1.0" />
<PackageReference Include="XT.Net" Version="3.1.0" />
</ItemGroup>
</Project>

View File

@ -6,20 +6,20 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="12.5.0" />
<PackageReference Include="Bitfinex.Net" Version="10.6.0" />
<PackageReference Include="BitMart.Net" Version="3.5.0" />
<PackageReference Include="Bybit.Net" Version="6.5.0" />
<PackageReference Include="CoinEx.Net" Version="10.5.0" />
<PackageReference Include="CryptoCom.Net" Version="3.5.0" />
<PackageReference Include="GateIo.Net" Version="3.5.0" />
<PackageReference Include="JK.Bitget.Net" Version="3.5.0" />
<PackageReference Include="JK.Mexc.Net" Version="4.5.0" />
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="3.5.1" />
<PackageReference Include="JKorf.HTX.Net" Version="8.5.0" />
<PackageReference Include="KrakenExchange.Net" Version="7.5.0" />
<PackageReference Include="Kucoin.Net" Version="8.5.0" />
<PackageReference Include="Binance.Net" Version="12.1.0" />
<PackageReference Include="Bitfinex.Net" Version="10.2.0" />
<PackageReference Include="BitMart.Net" Version="3.1.0" />
<PackageReference Include="Bybit.Net" Version="6.1.0" />
<PackageReference Include="CoinEx.Net" Version="10.1.0" />
<PackageReference Include="CryptoCom.Net" Version="3.1.0" />
<PackageReference Include="GateIo.Net" Version="3.1.0" />
<PackageReference Include="JK.Bitget.Net" Version="3.1.0" />
<PackageReference Include="JK.Mexc.Net" Version="4.1.0" />
<PackageReference Include="JK.OKX.Net" Version="4.1.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="3.1.0" />
<PackageReference Include="JKorf.HTX.Net" Version="8.1.0" />
<PackageReference Include="KrakenExchange.Net" Version="7.1.0" />
<PackageReference Include="Kucoin.Net" Version="8.1.0" />
</ItemGroup>
</Project>

View File

@ -8,9 +8,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="12.5.0" />
<PackageReference Include="BitMart.Net" Version="3.5.0" />
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
<PackageReference Include="Binance.Net" Version="12.1.0" />
<PackageReference Include="BitMart.Net" Version="3.1.0" />
<PackageReference Include="JK.OKX.Net" Version="4.1.0" />
</ItemGroup>
</Project>

View File

@ -67,31 +67,6 @@ 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.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
* Fixed trading mode selection for futures listen key methods in FuturesUserDataTracker
* Version 10.5.0 - 10 Feb 2026
* Added keep alive for listenkeys to UserDataTracker
* Updated logging unmatched websocket message
* Updated websocket message forwarding logic
* Fixed bug in IncomingKbps calculation
* Fixed bug in SendAsync in SocketConnection
* Fixed bug in UserDataTracker orders logic incorrectly setting order to canceled status
* Version 10.4.1 - 06 Feb 2026
* Updated UserDataTracker to only track symbol when position size > 0
* Update UserDataTracker log verbosity