diff --git a/CryptoExchange.Net.UnitTests/ExchangeSymbolCacheTests.cs b/CryptoExchange.Net.UnitTests/ExchangeSymbolCacheTests.cs new file mode 100644 index 0000000..c11c41b --- /dev/null +++ b/CryptoExchange.Net.UnitTests/ExchangeSymbolCacheTests.cs @@ -0,0 +1,465 @@ +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(); + + // 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()); + + // 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); + } + + } +} \ No newline at end of file diff --git a/CryptoExchange.Net.UnitTests/SharedQuantityTests.cs b/CryptoExchange.Net.UnitTests/SharedQuantityTests.cs new file mode 100644 index 0000000..ec65f97 --- /dev/null +++ b/CryptoExchange.Net.UnitTests/SharedQuantityTests.cs @@ -0,0 +1,633 @@ +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)); + } + } +} \ No newline at end of file diff --git a/CryptoExchange.Net.UnitTests/SharedSymbolTests.cs b/CryptoExchange.Net.UnitTests/SharedSymbolTests.cs new file mode 100644 index 0000000..1d27b78 --- /dev/null +++ b/CryptoExchange.Net.UnitTests/SharedSymbolTests.cs @@ -0,0 +1,400 @@ +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( + (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( + (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( + (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( + (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( + (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( + (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( + (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)); + } + } +} \ No newline at end of file diff --git a/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs index c0de716..413a434 100644 --- a/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/Default/CryptoExchangeWebSocketClient.cs @@ -87,7 +87,8 @@ namespace CryptoExchange.Net.Sockets.Default get { UpdateReceivedMessages(); - return Math.Round(_prevSlotBytesReceived * (_lastBytesReceivedUpdate - _prevSlotBytesReceivedUpdate).TotalSeconds / 1000); + var seconds = (_lastBytesReceivedUpdate - _prevSlotBytesReceivedUpdate).TotalSeconds; + return seconds > 0 ? Math.Round(_prevSlotBytesReceived / seconds / 1000) : 0; } } diff --git a/CryptoExchange.Net/Sockets/Default/SocketConnection.cs b/CryptoExchange.Net/Sockets/Default/SocketConnection.cs index dff53dd..f4c46df 100644 --- a/CryptoExchange.Net/Sockets/Default/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/Default/SocketConnection.cs @@ -948,7 +948,7 @@ namespace CryptoExchange.Net.Sockets.Default return SendStringAsync(requestId, str, weight); str = stringSerializer.Serialize(obj); - return SendAsync(requestId, str, weight); + return SendStringAsync(requestId, str, weight); } throw new Exception("Unknown serializer when sending message");