mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 00:43:03 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 813bd9f5a1 | |||
| c8d2b4f09d | |||
| 6560b82a3e | |||
| e151af8f37 | |||
| bdf7a07c6f | |||
| a8ffe90bf2 | |||
| 3372b9eb44 | |||
| df25221960 | |||
| 7c67a014f5 | |||
| 65291b3195 | |||
| ece6a9d27d | |||
| 1ed29b0474 | |||
| be2eb01353 | |||
| 39cd66596d | |||
| 63f51811a9 | |||
| abda065237 | |||
| 759c8b9a58 | |||
| 8e18482781 | |||
| 87dd2d9d40 | |||
| f2b0cb2f0d |
@@ -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<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -304,55 +304,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleSubscriptionComplete(bool success, object? response)
|
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false);
|
||||||
{
|
if (!subscribeResult)
|
||||||
if (!success)
|
return new CallResult<UpdateSubscription>(subscribeResult.Error!);
|
||||||
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);
|
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
|
||||||
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (!_unknownValuesWarned.Contains(stringValue))
|
if (!_unknownValuesWarned.Contains(stringValue))
|
||||||
{
|
{
|
||||||
_unknownValuesWarned.Add(stringValue!);
|
_unknownValuesWarned.Add(stringValue!);
|
||||||
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");
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +246,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
// If no explicit mapping is found try to parse string
|
// If no explicit mapping is found try to parse string
|
||||||
result = (T)Enum.Parse(objectType, value, true);
|
result = (T)Enum.Parse(objectType, value, true);
|
||||||
|
if (!Enum.IsDefined(objectType, result))
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||||
<PackageVersion>10.4.0</PackageVersion>
|
<PackageVersion>10.5.3</PackageVersion>
|
||||||
<AssemblyVersion>10.4.0</AssemblyVersion>
|
<AssemblyVersion>10.5.3</AssemblyVersion>
|
||||||
<FileVersion>10.4.0</FileVersion>
|
<FileVersion>10.5.3</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
|
|||||||
@@ -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, Exception?> _sendingPeriodic;
|
||||||
private static readonly Action<ILogger, int, string, string, Exception?> _periodicSendFailed;
|
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, int, string, Exception?> _sendingData;
|
||||||
private static readonly Action<ILogger, int, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
|
private static readonly Action<ILogger, int, string, string, string, Exception?> _receivedMessageNotMatchedToAnyListener;
|
||||||
private static readonly Action<ILogger, int, int, int, Exception?> _sendingByteData;
|
private static readonly Action<ILogger, int, int, int, Exception?> _sendingByteData;
|
||||||
|
|
||||||
static SocketConnectionLoggingExtension()
|
static SocketConnectionLoggingExtension()
|
||||||
@@ -177,10 +177,10 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
new EventId(2028, "SendingData"),
|
new EventId(2028, "SendingData"),
|
||||||
"[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}");
|
"[Sckt {SocketId}] [Req {RequestId}] sending message: {Data}");
|
||||||
|
|
||||||
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string>(
|
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string, string>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
|
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
|
||||||
"[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: [{ListenIds}]");
|
"[Sckt {SocketId}] received message not matched to any listener. TypeIdentifier: {TypeIdentifier}, ListenId: {ListenId}, current listeners: [{ListenIds}]");
|
||||||
|
|
||||||
_failedToParse = LoggerMessage.Define<int, string>(
|
_failedToParse = LoggerMessage.Define<int, string>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
@@ -326,9 +326,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_sendingData(logger, socketId, requestId, data, null);
|
_sendingData(logger, socketId, requestId, data, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string listenId, string listenIds)
|
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string typeIdentifier, string listenId, string listenIds)
|
||||||
{
|
{
|
||||||
_receivedMessageNotMatchedToAnyListener(logger, socketId, listenId, listenIds, null);
|
_receivedMessageNotMatchedToAnyListener(logger, socketId, typeIdentifier, listenId, listenIds, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
|
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
UpdateReceivedMessages();
|
UpdateReceivedMessages();
|
||||||
return Math.Round(_prevSlotBytesReceived * (_lastBytesReceivedUpdate - _prevSlotBytesReceivedUpdate).TotalSeconds / 1000);
|
var seconds = (_lastBytesReceivedUpdate - _prevSlotBytesReceivedUpdate).TotalSeconds;
|
||||||
|
return seconds > 0 ? Math.Round(_prevSlotBytesReceived / seconds / 1000) : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -633,22 +633,37 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (route.TypeIdentifier != typeIdentifier)
|
if (route.TypeIdentifier != typeIdentifier)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (topicFilter == null
|
// Forward message rules:
|
||||||
|| route.TopicFilter == null
|
// | Message Topic | Route Topic Filter | Topics Match | Forward | Description
|
||||||
|| route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal))
|
// | 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)
|
||||||
{
|
{
|
||||||
processed = true;
|
if (route.TopicFilter != null)
|
||||||
|
// No topic on message, but route is filtering on topic
|
||||||
if (isQuery && query!.Completed)
|
|
||||||
continue;
|
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)
|
if (complete)
|
||||||
@@ -658,10 +673,16 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
|
|
||||||
if (!processed)
|
if (!processed)
|
||||||
{
|
{
|
||||||
lock (_listenersLock)
|
if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data))
|
||||||
{
|
{
|
||||||
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, topicFilter!,
|
lock (_listenersLock)
|
||||||
string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]")))));
|
{
|
||||||
|
_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]")))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -930,7 +951,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
return SendStringAsync(requestId, str, weight);
|
return SendStringAsync(requestId, str, weight);
|
||||||
|
|
||||||
str = stringSerializer.Serialize(obj);
|
str = stringSerializer.Serialize(obj);
|
||||||
return SendAsync(requestId, str, weight);
|
return SendStringAsync(requestId, str, weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Exception("Unknown serializer when sending message");
|
throw new Exception("Unknown serializer when sending message");
|
||||||
@@ -1064,40 +1085,8 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
var taskList = new List<Task<CallResult>>();
|
var taskList = new List<Task<CallResult>>();
|
||||||
foreach (var subscription in subList)
|
foreach (var subscription in subList)
|
||||||
{
|
{
|
||||||
subscription.ConnectionInvocations = 0;
|
var subscribeTask = TrySubscribeAsync(subscription, false, default);
|
||||||
if (!subscription.Active)
|
taskList.Add(subscribeTask);
|
||||||
// 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);
|
await Task.WhenAll(taskList).ConfigureAwait(false);
|
||||||
@@ -1114,6 +1103,61 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
return CallResult.SuccessResult;
|
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)
|
internal async Task UnsubscribeAsync(Subscription subscription)
|
||||||
{
|
{
|
||||||
var unsubscribeRequest = subscription.CreateUnsubscriptionQuery(this);
|
var unsubscribeRequest = subscription.CreateUnsubscriptionQuery(this);
|
||||||
|
|||||||
@@ -174,6 +174,14 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
ConnectionInvocations++;
|
ConnectionInvocations++;
|
||||||
TotalInvocations++;
|
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);
|
return route.Handle(connection, receiveTime, originalData, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,8 +127,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Completed = true;
|
|
||||||
Result = CallResult.SuccessResult;
|
Result = CallResult.SuccessResult;
|
||||||
|
Completed = true;
|
||||||
_event.Set();
|
_event.Set();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,12 +216,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (Completed)
|
if (Completed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Completed = true;
|
|
||||||
if (TimeoutBehavior == TimeoutBehavior.Fail)
|
if (TimeoutBehavior == TimeoutBehavior.Fail)
|
||||||
Result = new CallResult<THandlerResponse>(new TimeoutError());
|
Result = new CallResult<THandlerResponse>(new TimeoutError());
|
||||||
else
|
else
|
||||||
Result = new CallResult<THandlerResponse>(default, null, default);
|
Result = new CallResult<THandlerResponse>(default, null, default);
|
||||||
|
|
||||||
|
Completed = true;
|
||||||
_event.Set();
|
_event.Set();
|
||||||
OnComplete?.Invoke();
|
OnComplete?.Invoke();
|
||||||
}
|
}
|
||||||
@@ -234,6 +234,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
Result = new CallResult<THandlerResponse>(error);
|
Result = new CallResult<THandlerResponse>(error);
|
||||||
Completed = true;
|
Completed = true;
|
||||||
|
|
||||||
_event.Set();
|
_event.Set();
|
||||||
OnComplete?.Invoke();
|
OnComplete?.Invoke();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
private readonly IFuturesOrderSocketClient? _socketClient;
|
private readonly IFuturesOrderSocketClient? _socketClient;
|
||||||
private readonly ExchangeParameters? _exchangeParameters;
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
private readonly bool _requiresSymbolParameterOpenOrders;
|
private readonly bool _requiresSymbolParameterOpenOrders;
|
||||||
|
private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new();
|
||||||
|
|
||||||
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
||||||
|
|
||||||
@@ -129,6 +130,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
// status changed from open to not open
|
// status changed from open to not open
|
||||||
return true;
|
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)
|
if (existingItem.Status != SharedOrderStatus.Open && updateItem.Status == SharedOrderStatus.Open)
|
||||||
// status changed from not open to open; stale
|
// status changed from not open to open; stale
|
||||||
return false;
|
return false;
|
||||||
@@ -243,10 +252,38 @@ 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))
|
||||||
|
{
|
||||||
|
if (openOrders.Any(x => x.OrderId == order.OrderId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!_openOrderNotReturnedTimes.ContainsKey(order.OrderId))
|
||||||
|
_openOrderNotReturnedTimes[order.OrderId] = 0;
|
||||||
|
|
||||||
|
_openOrderNotReturnedTimes[order.OrderId] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var symbol in _symbols.ToList())
|
foreach (var symbol in _symbols.ToList())
|
||||||
{
|
{
|
||||||
|
// Determine the timestamp from which we need to check order status
|
||||||
|
// Use the timestamp we last know the correct state of the data
|
||||||
var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
||||||
|
// If we're tracking open orders with a create time before this time we need to use that timestamp to make sure that order is included in the response
|
||||||
|
var trackedOrdersMinOpenTime = Values
|
||||||
|
.Where(x => x.Status == SharedOrderStatus.Open && x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset)
|
||||||
|
.OrderBy(x => x.CreateTime)
|
||||||
|
.FirstOrDefault()?.CreateTime;
|
||||||
|
if (trackedOrdersMinOpenTime != null && trackedOrdersMinOpenTime < fromTimeOrders)
|
||||||
|
fromTimeOrders = trackedOrdersMinOpenTime.Value.AddMilliseconds(-1);
|
||||||
|
|
||||||
var updatedPollTime = DateTime.UtcNow;
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
var closedOrdersResult = await _restClient.GetClosedFuturesOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var closedOrdersResult = await _restClient.GetClosedFuturesOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!closedOrdersResult.Success)
|
if (!closedOrdersResult.Success)
|
||||||
@@ -264,17 +301,23 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
|
|
||||||
// Filter orders to only include where close time is after the start time
|
// Filter orders to only include where close time is after the start time
|
||||||
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
||||||
x.UpdateTime != null && x.UpdateTime >= _startTime // Updated after the tracker start time
|
(x.UpdateTime != null && x.UpdateTime >= _startTime) // Updated after the tracker start time
|
||||||
|| x.CreateTime != null && x.CreateTime >= _startTime // Created after the tracker start time
|
|| (x.CreateTime != null && x.CreateTime >= _startTime) // Created after the tracker start time
|
||||||
|| x.CreateTime == null && x.UpdateTime == null // Unknown time
|
|| (x.CreateTime == null && x.UpdateTime == null) // Unknown time
|
||||||
).ToArray();
|
).ToArray();
|
||||||
|
|
||||||
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
||||||
var openOrdersNotReturned = Values.Where(x =>
|
var openOrdersNotReturned = Values.Where(x =>
|
||||||
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset // Orders for the same symbol
|
// Orders for the same symbol
|
||||||
&& x.QuantityFilled?.IsZero == true // With no filled value
|
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset
|
||||||
&& !openOrders.Any(r => r.OrderId == x.OrderId) // Not returned in open orders
|
// With no filled value
|
||||||
&& !relevantOrders.Any(r => r.OrderId == x.OrderId) // Not return in closed orders
|
&& 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)
|
||||||
).ToList();
|
).ToList();
|
||||||
|
|
||||||
var additionalUpdates = new List<SharedFuturesOrder>();
|
var additionalUpdates = new List<SharedFuturesOrder>();
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
@event = @event.Except(toRemove).ToArray();
|
@event = @event.Except(toRemove).ToArray();
|
||||||
|
|
||||||
if (!_onlyTrackProvidedSymbols)
|
if (!_onlyTrackProvidedSymbols)
|
||||||
UpdateSymbolsList(@event.OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
UpdateSymbolsList(@event.Where(x => x.PositionSize > 0).OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
||||||
|
|
||||||
|
|
||||||
// Update local store
|
// Update local store
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
private readonly ISpotOrderSocketClient? _socketClient;
|
private readonly ISpotOrderSocketClient? _socketClient;
|
||||||
private readonly ExchangeParameters? _exchangeParameters;
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
private readonly bool _requiresSymbolParameterOpenOrders;
|
private readonly bool _requiresSymbolParameterOpenOrders;
|
||||||
|
private readonly Dictionary<string, int> _openOrderNotReturnedTimes = new();
|
||||||
|
|
||||||
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
||||||
|
|
||||||
@@ -140,6 +141,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
// status changed from open to not open
|
// status changed from open to not open
|
||||||
return true;
|
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)
|
if (existingItem.Status != SharedOrderStatus.Open && updateItem.Status == SharedOrderStatus.Open)
|
||||||
// status changed from not open to open; stale
|
// status changed from not open to open; stale
|
||||||
return false;
|
return false;
|
||||||
@@ -258,9 +267,34 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
if (!_firstPollDone && anyError)
|
if (!_firstPollDone && anyError)
|
||||||
return 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))
|
||||||
|
{
|
||||||
|
if (openOrders.Any(x => x.OrderId == order.OrderId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!_openOrderNotReturnedTimes.ContainsKey(order.OrderId))
|
||||||
|
_openOrderNotReturnedTimes[order.OrderId] = 0;
|
||||||
|
|
||||||
|
_openOrderNotReturnedTimes[order.OrderId] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var symbol in _symbols.ToList())
|
foreach (var symbol in _symbols.ToList())
|
||||||
{
|
{
|
||||||
|
// Determine the timestamp from which we need to check order status
|
||||||
|
// Use the timestamp we last know the correct state of the data
|
||||||
var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
||||||
|
// If we're tracking open orders with a create time before this time we need to use that timestamp to make sure that order is included in the response
|
||||||
|
var trackedOrdersMinOpenTime = Values
|
||||||
|
.Where(x => x.Status == SharedOrderStatus.Open && x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset)
|
||||||
|
.OrderBy(x => x.CreateTime)
|
||||||
|
.FirstOrDefault()?.CreateTime;
|
||||||
|
if (trackedOrdersMinOpenTime != null && trackedOrdersMinOpenTime < fromTimeOrders)
|
||||||
|
fromTimeOrders = trackedOrdersMinOpenTime.Value.AddMilliseconds(-1);
|
||||||
|
|
||||||
var updatedPollTime = DateTime.UtcNow;
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
var closedOrdersResult = await _restClient.GetClosedSpotOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var closedOrdersResult = await _restClient.GetClosedSpotOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!closedOrdersResult.Success)
|
if (!closedOrdersResult.Success)
|
||||||
@@ -278,17 +312,23 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
|
|
||||||
// Filter orders to only include where close time is after the start time
|
// Filter orders to only include where close time is after the start time
|
||||||
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
||||||
x.UpdateTime != null && x.UpdateTime >= _startTime // Updated after the tracker start time
|
(x.UpdateTime != null && x.UpdateTime >= _startTime) // Updated after the tracker start time
|
||||||
|| x.CreateTime != null && x.CreateTime >= _startTime // Created after the tracker start time
|
|| (x.CreateTime != null && x.CreateTime >= _startTime) // Created after the tracker start time
|
||||||
|| x.CreateTime == null && x.UpdateTime == null // Unknown time
|
|| (x.CreateTime == null && x.UpdateTime == null) // Unknown time
|
||||||
).ToArray();
|
).ToArray();
|
||||||
|
|
||||||
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
||||||
var openOrdersNotReturned = Values.Where(x =>
|
var openOrdersNotReturned = Values.Where(x =>
|
||||||
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset // Orders for the same symbol
|
// Orders for the same symbol
|
||||||
&& x.QuantityFilled?.IsZero == true // With no filled value
|
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset
|
||||||
&& !openOrders.Any(r => r.OrderId == x.OrderId) // Not returned in open orders
|
// With no filled value
|
||||||
&& !relevantOrders.Any(r => r.OrderId == x.OrderId) // Not return in closed orders
|
&& 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)
|
||||||
).ToList();
|
).ToList();
|
||||||
|
|
||||||
var additionalUpdates = new List<SharedSpotOrder>();
|
var additionalUpdates = new List<SharedSpotOrder>();
|
||||||
|
|||||||
@@ -411,7 +411,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Updated {DataType} {Item}", DataType, key);
|
_logger.LogTrace("Updated {DataType} {Item}", DataType, key);
|
||||||
LastChangeTime = DateTime.UtcNow;
|
LastChangeTime = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -421,7 +421,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
|
|
||||||
if (!existed)
|
if (!existed)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Added {DataType} {Item}", DataType, GetKey(item));
|
_logger.LogTrace("Added {DataType} {Item}", DataType, GetKey(item));
|
||||||
LastChangeTime = DateTime.UtcNow;
|
LastChangeTime = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Trackers.UserData
|
namespace CryptoExchange.Net.Trackers.UserData
|
||||||
@@ -22,6 +23,11 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
/// Listen key to use for subscriptions
|
/// Listen key to use for subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string? _listenKey;
|
protected string? _listenKey;
|
||||||
|
/// <summary>
|
||||||
|
/// Cts
|
||||||
|
/// </summary>
|
||||||
|
protected CancellationTokenSource? _cts;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of data trackers
|
/// List of data trackers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -68,6 +74,8 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<CallResult> StartAsync()
|
public async Task<CallResult> StartAsync()
|
||||||
{
|
{
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
|
||||||
foreach(var tracker in DataTrackers)
|
foreach(var tracker in DataTrackers)
|
||||||
tracker.OnConnectedChange += (x) => OnConnectedChange?.Invoke(tracker.DataType, x);
|
tracker.OnConnectedChange += (x) => OnConnectedChange?.Invoke(tracker.DataType, x);
|
||||||
|
|
||||||
@@ -100,12 +108,21 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
public async Task StopAsync()
|
public async Task StopAsync()
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Stopping UserDataTracker");
|
_logger.LogDebug("Stopping UserDataTracker");
|
||||||
|
_cts?.Cancel();
|
||||||
|
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
foreach (var dataTracker in DataTrackers)
|
foreach (var dataTracker in DataTrackers)
|
||||||
tasks.Add(dataTracker.StopAsync());
|
tasks.Add(dataTracker.StopAsync());
|
||||||
|
|
||||||
|
await DoStopAsync().ConfigureAwait(false);
|
||||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
_logger.LogDebug("Stopped UserDataTracker");
|
_logger.LogDebug("Stopped UserDataTracker");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop implementation
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual Task DoStopAsync() => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
private readonly IFuturesSymbolRestClient _symbolClient;
|
private readonly IFuturesSymbolRestClient _symbolClient;
|
||||||
private readonly IListenKeyRestClient? _listenKeyClient;
|
private readonly IListenKeyRestClient? _listenKeyClient;
|
||||||
private readonly ExchangeParameters? _exchangeParameters;
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
|
private readonly TradingMode _tradingMode;
|
||||||
|
private Task? _lkKeepAliveTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override UserDataItemTracker[] DataTrackers { get; }
|
protected override UserDataItemTracker[] DataTrackers { get; }
|
||||||
@@ -68,10 +70,14 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
_listenKeyClient = listenKeyRestClient;
|
_listenKeyClient = listenKeyRestClient;
|
||||||
_exchangeParameters = exchangeParameters;
|
_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 trackers = new List<UserDataItemTracker>();
|
||||||
|
|
||||||
var balanceAccountType = accountType ?? SharedAccountType.PerpetualLinearFutures;
|
var balanceTracker = new BalanceTracker(logger, balanceRestClient, balanceSocketClient, accountType ?? SharedAccountType.PerpetualLinearFutures, config.BalancesConfig, exchangeParameters);
|
||||||
var balanceTracker = new BalanceTracker(logger, balanceRestClient, balanceSocketClient, balanceAccountType, config.BalancesConfig, exchangeParameters);
|
|
||||||
Balances = balanceTracker;
|
Balances = balanceTracker;
|
||||||
trackers.Add(balanceTracker);
|
trackers.Add(balanceTracker);
|
||||||
|
|
||||||
@@ -99,7 +105,7 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override async Task<CallResult> DoStartAsync()
|
protected override async Task<CallResult> DoStartAsync()
|
||||||
{
|
{
|
||||||
var symbolResult = await _symbolClient.GetFuturesSymbolsAsync(new GetSymbolsRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var symbolResult = await _symbolClient.GetFuturesSymbolsAsync(new GetSymbolsRequest(_tradingMode, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!symbolResult)
|
if (!symbolResult)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Failed to start UserFuturesDataTracker; symbols request failed: {Error}", symbolResult.Error);
|
_logger.LogWarning("Failed to start UserFuturesDataTracker; symbols request failed: {Error}", symbolResult.Error);
|
||||||
@@ -108,17 +114,45 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
|
|
||||||
if (_listenKeyClient != null)
|
if (_listenKeyClient != null)
|
||||||
{
|
{
|
||||||
var lkResult = await _listenKeyClient.StartListenKeyAsync(new StartListenKeyRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var lkResult = await _listenKeyClient.StartListenKeyAsync(new StartListenKeyRequest(_tradingMode, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!lkResult)
|
if (!lkResult)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Failed to start UserFuturesDataTracker; listen key request failed: {Error}", lkResult.Error);
|
_logger.LogWarning("Failed to start UserFuturesDataTracker; listen key request failed: {Error}", lkResult.Error);
|
||||||
return lkResult;
|
return lkResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_lkKeepAliveTask = KeepAliveListenKeyAsync();
|
||||||
|
|
||||||
_listenKey = lkResult.Data;
|
_listenKey = lkResult.Data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CallResult.SuccessResult;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Linq;
|
|||||||
using CryptoExchange.Net.Trackers.UserData.Interfaces;
|
using CryptoExchange.Net.Trackers.UserData.Interfaces;
|
||||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
|
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Trackers.UserData
|
namespace CryptoExchange.Net.Trackers.UserData
|
||||||
{
|
{
|
||||||
@@ -18,6 +19,7 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
private readonly ISpotSymbolRestClient _symbolClient;
|
private readonly ISpotSymbolRestClient _symbolClient;
|
||||||
private readonly IListenKeyRestClient? _listenKeyClient;
|
private readonly IListenKeyRestClient? _listenKeyClient;
|
||||||
private readonly ExchangeParameters? _exchangeParameters;
|
private readonly ExchangeParameters? _exchangeParameters;
|
||||||
|
private Task? _lkKeepAliveTask;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override UserDataItemTracker[] DataTrackers { get; }
|
protected override UserDataItemTracker[] DataTrackers { get; }
|
||||||
@@ -91,10 +93,39 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
return lkResult;
|
return lkResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_lkKeepAliveTask = KeepAliveListenKeyAsync();
|
||||||
|
|
||||||
_listenKey = lkResult.Data;
|
_listenKey = lkResult.Data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CallResult.SuccessResult;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,32 +5,32 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="12.1.0" />
|
<PackageReference Include="Binance.Net" Version="12.5.0" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="10.2.0" />
|
<PackageReference Include="Bitfinex.Net" Version="10.6.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="3.1.0" />
|
<PackageReference Include="BitMart.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="BloFin.Net" Version="2.1.1" />
|
<PackageReference Include="BloFin.Net" Version="2.5.0" />
|
||||||
<PackageReference Include="Bybit.Net" Version="6.1.0" />
|
<PackageReference Include="Bybit.Net" Version="6.5.0" />
|
||||||
<PackageReference Include="CoinEx.Net" Version="10.1.0" />
|
<PackageReference Include="CoinEx.Net" Version="10.5.0" />
|
||||||
<PackageReference Include="CoinW.Net" Version="2.1.1" />
|
<PackageReference Include="CoinW.Net" Version="2.5.0" />
|
||||||
<PackageReference Include="CryptoCom.Net" Version="3.1.0" />
|
<PackageReference Include="CryptoCom.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="DeepCoin.Net" Version="3.1.0" />
|
<PackageReference Include="DeepCoin.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="GateIo.Net" Version="3.1.0" />
|
<PackageReference Include="GateIo.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="HyperLiquid.Net" Version="3.2.0" />
|
<PackageReference Include="HyperLiquid.Net" Version="3.7.0" />
|
||||||
<PackageReference Include="JK.BingX.Net" Version="3.1.0" />
|
<PackageReference Include="JK.BingX.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="JK.Bitget.Net" Version="3.1.0" />
|
<PackageReference Include="JK.Bitget.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="JK.Mexc.Net" Version="4.1.0" />
|
<PackageReference Include="JK.Mexc.Net" Version="4.5.0" />
|
||||||
<PackageReference Include="JK.OKX.Net" Version="4.1.0" />
|
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
|
||||||
<PackageReference Include="Jkorf.Aster.Net" Version="2.1.0" />
|
<PackageReference Include="Jkorf.Aster.Net" Version="2.5.0" />
|
||||||
<PackageReference Include="JKorf.BitMEX.Net" Version="3.1.0" />
|
<PackageReference Include="JKorf.BitMEX.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="JKorf.Coinbase.Net" Version="3.1.0" />
|
<PackageReference Include="JKorf.Coinbase.Net" Version="3.5.1" />
|
||||||
<PackageReference Include="JKorf.HTX.Net" Version="8.1.0" />
|
<PackageReference Include="JKorf.HTX.Net" Version="8.5.0" />
|
||||||
<PackageReference Include="JKorf.Upbit.Net" Version="2.1.0" />
|
<PackageReference Include="JKorf.Upbit.Net" Version="2.5.0" />
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="7.1.0" />
|
<PackageReference Include="KrakenExchange.Net" Version="7.5.0" />
|
||||||
<PackageReference Include="Kucoin.Net" Version="8.1.0" />
|
<PackageReference Include="Kucoin.Net" Version="8.5.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
<PackageReference Include="Toobit.Net" Version="2.1.0" />
|
<PackageReference Include="Toobit.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="WhiteBit.Net" Version="3.1.0" />
|
<PackageReference Include="WhiteBit.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="XT.Net" Version="3.1.0" />
|
<PackageReference Include="XT.Net" Version="3.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -6,20 +6,20 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="12.1.0" />
|
<PackageReference Include="Binance.Net" Version="12.5.0" />
|
||||||
<PackageReference Include="Bitfinex.Net" Version="10.2.0" />
|
<PackageReference Include="Bitfinex.Net" Version="10.6.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="3.1.0" />
|
<PackageReference Include="BitMart.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="Bybit.Net" Version="6.1.0" />
|
<PackageReference Include="Bybit.Net" Version="6.5.0" />
|
||||||
<PackageReference Include="CoinEx.Net" Version="10.1.0" />
|
<PackageReference Include="CoinEx.Net" Version="10.5.0" />
|
||||||
<PackageReference Include="CryptoCom.Net" Version="3.1.0" />
|
<PackageReference Include="CryptoCom.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="GateIo.Net" Version="3.1.0" />
|
<PackageReference Include="GateIo.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="JK.Bitget.Net" Version="3.1.0" />
|
<PackageReference Include="JK.Bitget.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="JK.Mexc.Net" Version="4.1.0" />
|
<PackageReference Include="JK.Mexc.Net" Version="4.5.0" />
|
||||||
<PackageReference Include="JK.OKX.Net" Version="4.1.0" />
|
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
|
||||||
<PackageReference Include="JKorf.Coinbase.Net" Version="3.1.0" />
|
<PackageReference Include="JKorf.Coinbase.Net" Version="3.5.1" />
|
||||||
<PackageReference Include="JKorf.HTX.Net" Version="8.1.0" />
|
<PackageReference Include="JKorf.HTX.Net" Version="8.5.0" />
|
||||||
<PackageReference Include="KrakenExchange.Net" Version="7.1.0" />
|
<PackageReference Include="KrakenExchange.Net" Version="7.5.0" />
|
||||||
<PackageReference Include="Kucoin.Net" Version="8.1.0" />
|
<PackageReference Include="Kucoin.Net" Version="8.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Binance.Net" Version="12.1.0" />
|
<PackageReference Include="Binance.Net" Version="12.5.0" />
|
||||||
<PackageReference Include="BitMart.Net" Version="3.1.0" />
|
<PackageReference Include="BitMart.Net" Version="3.5.0" />
|
||||||
<PackageReference Include="JK.OKX.Net" Version="4.1.0" />
|
<PackageReference Include="JK.OKX.Net" Version="4.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -67,6 +67,31 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
|||||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 10.5.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
|
||||||
|
|
||||||
* Version 10.4.0 - 06 Feb 2026
|
* Version 10.4.0 - 06 Feb 2026
|
||||||
* Added user data tracking logic
|
* Added user data tracking logic
|
||||||
* Added LastReceiveTime, SocketStatus and SubscriptionStatus properties to UpdateSubscription
|
* Added LastReceiveTime, SocketStatus and SubscriptionStatus properties to UpdateSubscription
|
||||||
|
|||||||
Reference in New Issue
Block a user