mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73fcb47b17 | |||
| d41ca3459e | |||
| bea2b2bd7b | |||
| b29cdc41f3 | |||
| 0ce2e778f4 | |||
| 36c2411d46 | |||
| 6d3e72745a | |||
| 51c74baa26 | |||
| 419e01d009 | |||
| 297eee0e1f | |||
| 6a9231b1e3 | |||
| e40f2a15b6 | |||
| b94085a27a | |||
| 5e083811df | |||
| 7dcf5cd6ea | |||
| 1471a4733f | |||
| f39d9f7cfb | |||
| 9fab8faa45 | |||
| 226f175343 | |||
| 813bd9f5a1 | |||
| c8d2b4f09d | |||
| 6560b82a3e | |||
| e151af8f37 | |||
| bdf7a07c6f | |||
| a8ffe90bf2 | |||
| 3372b9eb44 | |||
| df25221960 | |||
| 7c67a014f5 | |||
| 65291b3195 | |||
| ece6a9d27d | |||
| 1ed29b0474 | |||
| be2eb01353 | |||
| 39cd66596d | |||
| 63f51811a9 | |||
| abda065237 | |||
| 759c8b9a58 | |||
| 8e18482781 |
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,6 +81,25 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.That(result.Error is ServerError);
|
Assert.That(result.Error is ServerError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[TestCase]
|
||||||
|
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var client = new TestRestClient();
|
||||||
|
var response = "<html>...</html>";
|
||||||
|
client.SetErrorWithResponse(response, System.Net.HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = await client.Api1.Request<TestObject>();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
ClassicAssert.IsFalse(result.Success);
|
||||||
|
Assert.That(result.Error != null);
|
||||||
|
Assert.That(result.Error is DeserializeError);
|
||||||
|
Assert.That(result.Error.Message.Contains(response));
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,11 +19,14 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
private ErrorMapping _errorMapping = new ErrorMapping([]);
|
private ErrorMapping _errorMapping = new ErrorMapping([]);
|
||||||
public override JsonSerializerOptions Options => new JsonSerializerOptions();
|
public override JsonSerializerOptions Options => new JsonSerializerOptions();
|
||||||
|
|
||||||
public override ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
||||||
{
|
{
|
||||||
var errorData = JsonSerializer.Deserialize<TestError>(responseStream);
|
var result = await GetJsonDocument(responseStream).ConfigureAwait(false);
|
||||||
|
if (result.Item1 != null)
|
||||||
|
return result.Item1;
|
||||||
|
|
||||||
return new ValueTask<Error>(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage)));
|
var errorData = result.Item2.Deserialize<TestError>();
|
||||||
|
return new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -437,23 +437,19 @@ namespace CryptoExchange.Net.Clients
|
|||||||
responseStream = await response.GetResponseStreamAsync(cancellationToken).ConfigureAwait(false);
|
responseStream = await response.GetResponseStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
string? originalData = null;
|
string? originalData = null;
|
||||||
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
||||||
if (outputOriginalData || MessageHandler.RequiresSeekableStream)
|
if (outputOriginalData || MessageHandler.RequiresSeekableStream || !response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
// If we want to return the original string data from the stream, but still want to process it
|
// Create a seekable stream from the response stream if:
|
||||||
// we'll need to copy it as the stream isn't seekable, and thus we can only read it once
|
// 1. We need to output the original data
|
||||||
var memoryStream = new MemoryStream();
|
// 2. The message handler requires a seekable stream
|
||||||
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
// 3. The response indicates error and we want to output (part of) the returned data
|
||||||
using var reader = new StreamReader(memoryStream, Encoding.UTF8, false, 4096, true);
|
responseStream = await CopyStreamAsync(responseStream).ConfigureAwait(false);
|
||||||
if (outputOriginalData)
|
using var reader = new StreamReader(responseStream, Encoding.UTF8, false, 4096, true);
|
||||||
|
if (outputOriginalData)
|
||||||
{
|
{
|
||||||
memoryStream.Position = 0;
|
|
||||||
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
|
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
responseStream.Position = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue processing from the memory stream since the response stream is already read and we can't seek it
|
|
||||||
responseStream.Close();
|
|
||||||
memoryStream.Position = 0;
|
|
||||||
responseStream = memoryStream;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
|
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
|
||||||
@@ -479,13 +475,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string
|
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
error = await MessageHandler.ParseErrorResponse(
|
error = await MessageHandler.ParseErrorResponse(
|
||||||
(int)response.StatusCode,
|
(int)response.StatusCode,
|
||||||
response.ResponseHeaders,
|
response.ResponseHeaders,
|
||||||
responseStream).ConfigureAwait(false);
|
responseStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -769,6 +764,15 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<Stream> CopyStreamAsync(Stream responseStream)
|
||||||
|
{
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||||
|
responseStream.Close();
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
return memoryStream;
|
||||||
|
}
|
||||||
|
|
||||||
private bool ShouldCache(RequestDefinition definition)
|
private bool ShouldCache(RequestDefinition definition)
|
||||||
=> ClientOptions.CachingEnabled
|
=> ClientOptions.CachingEnabled
|
||||||
&& definition.Method == HttpMethod.Get
|
&& definition.Method == HttpMethod.Get
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+15
-1
@@ -18,6 +18,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
|||||||
public abstract class JsonRestMessageHandler : IRestMessageHandler
|
public abstract class JsonRestMessageHandler : IRestMessageHandler
|
||||||
{
|
{
|
||||||
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
|
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
|
||||||
|
private const int _errorResponseSnippetLimit = 128;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Empty rate limit error
|
/// Empty rate limit error
|
||||||
@@ -80,7 +81,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return (new ServerError(new ErrorInfo(ErrorType.DeserializationFailed, false, "Deserialization failed, invalid JSON"), ex), null);
|
var errorMsg = "Deserialization failed, invalid JSON";
|
||||||
|
if (stream.CanSeek)
|
||||||
|
{
|
||||||
|
var dataSnippet = new char[_errorResponseSnippetLimit];
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
|
||||||
|
var data = new string(dataSnippet, 0, written);
|
||||||
|
errorMsg += $": {data}";
|
||||||
|
if (data.Length == _errorResponseSnippetLimit)
|
||||||
|
errorMsg += " (truncated)";
|
||||||
|
}
|
||||||
|
|
||||||
|
var error = new DeserializeError(errorMsg, ex);
|
||||||
|
return (error, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
@@ -165,6 +165,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return type identifier for non-json messages
|
||||||
|
/// </summary>
|
||||||
|
protected virtual string? GetTypeIdentifierNonJson(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||||
{
|
{
|
||||||
@@ -173,6 +181,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
|||||||
int? arrayIndex = null;
|
int? arrayIndex = null;
|
||||||
|
|
||||||
_searchResult.Clear();
|
_searchResult.Clear();
|
||||||
|
if (data[0] != 0x5B && data[0] != 0x7B)
|
||||||
|
{
|
||||||
|
// Message doesn't start with `{` or `[`, not valid for processing as json
|
||||||
|
return GetTypeIdentifierNonJson(data, webSocketMessageType);
|
||||||
|
}
|
||||||
|
|
||||||
var reader = new Utf8JsonReader(data);
|
var reader = new Utf8JsonReader(data);
|
||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.1</PackageVersion>
|
<PackageVersion>10.7.0</PackageVersion>
|
||||||
<AssemblyVersion>10.4.1</AssemblyVersion>
|
<AssemblyVersion>10.7.0</AssemblyVersion>
|
||||||
<FileVersion>10.4.1</FileVersion>
|
<FileVersion>10.7.0</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>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using CryptoExchange.Net.SharedApis;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -310,11 +311,11 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="request">The request parameters</param>
|
/// <param name="request">The request parameters</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var result = new List<T>();
|
var result = new List<T>();
|
||||||
ExchangeWebResult<T[]> batch;
|
ExchangeWebResult<T[]> batch;
|
||||||
INextPageToken? nextPageToken = null;
|
PageRequest? nextPageToken = null;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
||||||
@@ -323,12 +324,42 @@ namespace CryptoExchange.Net
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
result.AddRange(batch.Data);
|
result.AddRange(batch.Data);
|
||||||
nextPageToken = batch.NextPageToken;
|
nextPageToken = batch.NextPageRequest;
|
||||||
if (nextPageToken == null)
|
if (nextPageToken == null)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply filters to the data set
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type</typeparam>
|
||||||
|
/// <param name="data">Data set</param>
|
||||||
|
/// <param name="timeSelector">Time selector for the data</param>
|
||||||
|
/// <param name="startTime">Start time filter</param>
|
||||||
|
/// <param name="endTime">End time filter</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
|
public static IEnumerable<T> ApplyFilter<T>(
|
||||||
|
IEnumerable<T> data,
|
||||||
|
Func<T, DateTime> timeSelector,
|
||||||
|
DateTime? startTime,
|
||||||
|
DateTime? endTime,
|
||||||
|
DataDirection direction)
|
||||||
|
{
|
||||||
|
if (direction == DataDirection.Ascending)
|
||||||
|
data = data.OrderBy(timeSelector);
|
||||||
|
else
|
||||||
|
data = data.OrderByDescending(timeSelector);
|
||||||
|
|
||||||
|
if (startTime != null)
|
||||||
|
data = data.Where(x => timeSelector(x) >= startTime.Value);
|
||||||
|
|
||||||
|
if (endTime != null)
|
||||||
|
data = data.Where(x => timeSelector(x) < endTime.Value);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
|
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -105,31 +106,36 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new HttpMessageHandler instance
|
/// Create a new HttpMessageHandler instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static HttpMessageHandler CreateHttpClientMessageHandler(ApiProxy? proxy, TimeSpan? keepAliveInterval)
|
public static HttpMessageHandler CreateHttpClientMessageHandler(RestExchangeOptions options)
|
||||||
{
|
{
|
||||||
#if NET5_0_OR_GREATER
|
#if NET5_0_OR_GREATER
|
||||||
var socketHandler = new SocketsHttpHandler();
|
var socketHandler = new SocketsHttpHandler();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (keepAliveInterval != null && keepAliveInterval != TimeSpan.Zero)
|
if (options.HttpKeepAliveInterval != null && options.HttpKeepAliveInterval != TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
|
socketHandler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
|
||||||
socketHandler.KeepAlivePingDelay = keepAliveInterval.Value;
|
socketHandler.KeepAlivePingDelay = options.HttpKeepAliveInterval.Value;
|
||||||
socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
|
socketHandler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
socketHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
|
socketHandler.DefaultProxyCredentials = CredentialCache.DefaultCredentials;
|
||||||
|
|
||||||
|
socketHandler.EnableMultipleHttp2Connections = options.HttpEnableMultipleHttp2Connections;
|
||||||
|
socketHandler.PooledConnectionLifetime = options.HttpPooledConnectionLifetime;
|
||||||
|
socketHandler.PooledConnectionIdleTimeout = options.HttpPooledConnectionIdleTimeout;
|
||||||
|
socketHandler.MaxConnectionsPerServer = options.HttpMaxConnectionsPerServer;
|
||||||
}
|
}
|
||||||
catch (PlatformNotSupportedException) { }
|
catch (PlatformNotSupportedException) { }
|
||||||
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
||||||
|
|
||||||
if (proxy != null)
|
if (options.Proxy != null)
|
||||||
{
|
{
|
||||||
socketHandler.Proxy = new WebProxy
|
socketHandler.Proxy = new WebProxy
|
||||||
{
|
{
|
||||||
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
Address = new Uri($"{options.Proxy.Host}:{options.Proxy.Port}"),
|
||||||
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
Credentials = options.Proxy.Password == null ? null : new NetworkCredential(options.Proxy.Login, options.Proxy.Password)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return socketHandler;
|
return socketHandler;
|
||||||
@@ -143,12 +149,12 @@ namespace CryptoExchange.Net
|
|||||||
catch (PlatformNotSupportedException) { }
|
catch (PlatformNotSupportedException) { }
|
||||||
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
catch (NotImplementedException) { } // Mono runtime throws NotImplementedException
|
||||||
|
|
||||||
if (proxy != null)
|
if (options.Proxy != null)
|
||||||
{
|
{
|
||||||
httpHandler.Proxy = new WebProxy
|
httpHandler.Proxy = new WebProxy
|
||||||
{
|
{
|
||||||
Address = new Uri($"{proxy.Host}:{proxy.Port}"),
|
Address = new Uri($"{options.Proxy.Host}:{options.Proxy.Port}"),
|
||||||
Credentials = proxy.Password == null ? null : new NetworkCredential(proxy.Login, proxy.Password)
|
Credentials = options.Proxy.Password == null ? null : new NetworkCredential(options.Proxy.Login, options.Proxy.Password)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return httpHandler;
|
return httpHandler;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -531,11 +531,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="exchange">The exchange</param>
|
/// <param name="exchange">The exchange</param>
|
||||||
/// <param name="tradeMode">Trade mode the result applies to</param>
|
/// <param name="tradeMode">Trade mode the result applies to</param>
|
||||||
/// <param name="data">Data</param>
|
/// <param name="data">Data</param>
|
||||||
/// <param name="nextPageToken">Next page token</param>
|
/// <param name="nextPageRequest">Next page request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, INextPageToken? nextPageToken = null)
|
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode tradeMode, [AllowNull] K data, PageRequest? nextPageRequest = null)
|
||||||
{
|
{
|
||||||
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageToken);
|
return new ExchangeWebResult<K>(exchange, tradeMode, As<K>(data), nextPageRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -545,11 +545,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="exchange">The exchange</param>
|
/// <param name="exchange">The exchange</param>
|
||||||
/// <param name="tradeModes">Trade modes the result applies to</param>
|
/// <param name="tradeModes">Trade modes the result applies to</param>
|
||||||
/// <param name="data">Data</param>
|
/// <param name="data">Data</param>
|
||||||
/// <param name="nextPageToken">Next page token</param>
|
/// <param name="nextPageRequest">Next page token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, INextPageToken? nextPageToken = null)
|
public ExchangeWebResult<K> AsExchangeResult<K>(string exchange, TradingMode[]? tradeModes, [AllowNull] K data, PageRequest? nextPageRequest = null)
|
||||||
{
|
{
|
||||||
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageToken);
|
return new ExchangeWebResult<K>(exchange, tradeModes, As<K>(data), nextPageRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -211,7 +211,15 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DeserializeError(string? message = null, Exception? exception = null) : base(null, _errorInfo with { Message = (message?.Length > 0 ? _errorInfo.Message + ": " + message : _errorInfo.Message) }, exception) { }
|
public DeserializeError(string? message = null, Exception? exception = null)
|
||||||
|
: base(null,
|
||||||
|
_errorInfo with
|
||||||
|
{
|
||||||
|
Message = message?.Length > 0
|
||||||
|
? message
|
||||||
|
: _errorInfo.Message
|
||||||
|
},
|
||||||
|
exception) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -32,10 +32,29 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
#else
|
#else
|
||||||
= new Version(1, 1);
|
= new Version(1, 1);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Http client keep alive interval for keeping connections open
|
/// Http client keep alive interval for keeping connections open. Only applied when using dotnet8.0 or higher and dependency injection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? HttpKeepAliveInterval { get; set; } = TimeSpan.FromSeconds(15);
|
public TimeSpan? HttpKeepAliveInterval { get; set; } = TimeSpan.FromSeconds(15);
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
/// <summary>
|
||||||
|
/// Enable multiple simultaneous HTTP 2 connections. Only applied when using dependency injection
|
||||||
|
/// </summary>
|
||||||
|
public bool HttpEnableMultipleHttp2Connections { get; set; } = false;
|
||||||
|
/// <summary>
|
||||||
|
/// Lifetime of pooled HTTP connections; the time before a connection is recreated. Only applied when using dependency injection
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan HttpPooledConnectionLifetime { get; set; } = TimeSpan.FromMinutes(15);
|
||||||
|
/// <summary>
|
||||||
|
/// Idle timeout of pooled HTTP connections; the time before an open connection is closed when there are no requests. Only applied when using dependency injection
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan HttpPooledConnectionIdleTimeout { get; set; } = TimeSpan.FromMinutes(2);
|
||||||
|
/// <summary>
|
||||||
|
/// Max number of connections per server. Only applied when using dependency injection
|
||||||
|
/// </summary>
|
||||||
|
public int HttpMaxConnectionsPerServer { get; set; } = int.MaxValue;
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the values of this options on the target options
|
/// Set the values of this options on the target options
|
||||||
@@ -54,6 +73,12 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
item.CachingMaxAge = CachingMaxAge;
|
item.CachingMaxAge = CachingMaxAge;
|
||||||
item.HttpVersion = HttpVersion;
|
item.HttpVersion = HttpVersion;
|
||||||
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
|
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
item.HttpMaxConnectionsPerServer = HttpMaxConnectionsPerServer;
|
||||||
|
item.HttpPooledConnectionLifetime = HttpPooledConnectionLifetime;
|
||||||
|
item.HttpPooledConnectionIdleTimeout = HttpPooledConnectionIdleTimeout;
|
||||||
|
item.HttpEnableMultipleHttp2Connections = HttpEnableMultipleHttp2Connections;
|
||||||
|
#endif
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,6 +170,6 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString() => base.ToString().TrimEnd('-') + Data?.ToString();
|
public override string ToString() => base.ToString().TrimEnd(' ', '-') + " - " + Data?.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,7 +332,6 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
public async Task StopAsync()
|
public async Task StopAsync()
|
||||||
{
|
{
|
||||||
_logger.OrderBookStopping(Api, Symbol);
|
_logger.OrderBookStopping(Api, Symbol);
|
||||||
Status = OrderBookStatus.Disconnected;
|
|
||||||
_cts?.Cancel();
|
_cts?.Cancel();
|
||||||
_queueEvent.Set();
|
_queueEvent.Set();
|
||||||
if (_processTask != null)
|
if (_processTask != null)
|
||||||
@@ -345,6 +344,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
_subscription.ConnectionRestored -= HandleConnectionRestored;
|
_subscription.ConnectionRestored -= HandleConnectionRestored;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Status = OrderBookStatus.Disconnected;
|
||||||
_logger.OrderBookStopped(Api, Symbol);
|
_logger.OrderBookStopped(Api, Symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,6 +448,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
DateTime? serverDataTime = null,
|
DateTime? serverDataTime = null,
|
||||||
DateTime? localDataTime = null)
|
DateTime? localDataTime = null)
|
||||||
{
|
{
|
||||||
|
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
|
||||||
|
throw new InvalidOperationException("Trying to set snapshot while book is not working");
|
||||||
|
|
||||||
_processQueue.Enqueue(
|
_processQueue.Enqueue(
|
||||||
new OrderBookSnapshot
|
new OrderBookSnapshot
|
||||||
{
|
{
|
||||||
@@ -475,6 +478,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
DateTime? serverDataTime = null,
|
DateTime? serverDataTime = null,
|
||||||
DateTime? localDataTime = null)
|
DateTime? localDataTime = null)
|
||||||
{
|
{
|
||||||
|
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
|
||||||
|
throw new InvalidOperationException("Trying to update order book while book is not working");
|
||||||
|
|
||||||
_processQueue.Enqueue(
|
_processQueue.Enqueue(
|
||||||
new OrderBookUpdate
|
new OrderBookUpdate
|
||||||
{
|
{
|
||||||
@@ -505,6 +511,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
DateTime? serverDataTime = null,
|
DateTime? serverDataTime = null,
|
||||||
DateTime? localDataTime = null)
|
DateTime? localDataTime = null)
|
||||||
{
|
{
|
||||||
|
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
|
||||||
|
throw new InvalidOperationException("Trying to update order book while book is not working");
|
||||||
|
|
||||||
_processQueue.Enqueue(
|
_processQueue.Enqueue(
|
||||||
new OrderBookUpdate
|
new OrderBookUpdate
|
||||||
{
|
{
|
||||||
@@ -531,6 +540,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
DateTime? serverDataTime = null,
|
DateTime? serverDataTime = null,
|
||||||
DateTime? localDataTime = null)
|
DateTime? localDataTime = null)
|
||||||
{
|
{
|
||||||
|
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
|
||||||
|
throw new InvalidOperationException("Trying to update order book while book is not working");
|
||||||
|
|
||||||
var highest = Math.Max(bids.Any() ? bids.Max(b => b.Sequence) : 0, asks.Any() ? asks.Max(a => a.Sequence) : 0);
|
var highest = Math.Max(bids.Any() ? bids.Max(b => b.Sequence) : 0, asks.Any() ? asks.Max(a => a.Sequence) : 0);
|
||||||
var lowest = Math.Min(bids.Any() ? bids.Min(b => b.Sequence) : long.MaxValue, asks.Any() ? asks.Min(a => a.Sequence) : long.MaxValue);
|
var lowest = Math.Min(bids.Any() ? bids.Min(b => b.Sequence) : long.MaxValue, asks.Any() ? asks.Min(a => a.Sequence) : long.MaxValue);
|
||||||
|
|
||||||
@@ -554,6 +566,9 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// <param name="sequenceNumber">The sequence number of the message if it's a separate message with separate number</param>
|
/// <param name="sequenceNumber">The sequence number of the message if it's a separate message with separate number</param>
|
||||||
protected void AddChecksum(int checksum, long? sequenceNumber = null)
|
protected void AddChecksum(int checksum, long? sequenceNumber = null)
|
||||||
{
|
{
|
||||||
|
if (Status == OrderBookStatus.Disposed || Status == OrderBookStatus.Disconnected)
|
||||||
|
throw new InvalidOperationException("Trying to add checksum while book is not working");
|
||||||
|
|
||||||
_processQueue.Enqueue(new OrderBookChecksum() { Checksum = checksum, SequenceNumber = sequenceNumber });
|
_processQueue.Enqueue(new OrderBookChecksum() { Checksum = checksum, SequenceNumber = sequenceNumber });
|
||||||
_queueEvent.Set();
|
_queueEvent.Set();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
|||||||
/// <param name="path"></param>
|
/// <param name="path"></param>
|
||||||
public PathStartFilter(string path)
|
public PathStartFilter(string path)
|
||||||
{
|
{
|
||||||
_path = path;
|
_path = path.TrimStart('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||||
=> definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
|
=> definition.Path.TrimStart('/').StartsWith(_path, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,16 @@ namespace CryptoExchange.Net.Requests
|
|||||||
public class RequestFactory : IRequestFactory
|
public class RequestFactory : IRequestFactory
|
||||||
{
|
{
|
||||||
private HttpClient? _httpClient;
|
private HttpClient? _httpClient;
|
||||||
|
private RestExchangeOptions? _options;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Configure(RestExchangeOptions options, HttpClient? client = null)
|
public void Configure(RestExchangeOptions options, HttpClient? client = null)
|
||||||
{
|
{
|
||||||
if (client == null)
|
if (client == null)
|
||||||
client = CreateClient(options.Proxy, options.RequestTimeout, options.HttpKeepAliveInterval);
|
client = CreateClient(options);
|
||||||
|
|
||||||
_httpClient = client;
|
_httpClient = client;
|
||||||
|
_options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -39,15 +41,20 @@ namespace CryptoExchange.Net.Requests
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
|
public void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
|
||||||
{
|
{
|
||||||
_httpClient = CreateClient(proxy, requestTimeout, httpKeepAliveInterval);
|
var newOptions = new RestExchangeOptions();
|
||||||
|
_options!.Set(newOptions);
|
||||||
|
newOptions.Proxy = proxy;
|
||||||
|
newOptions.RequestTimeout = requestTimeout;
|
||||||
|
newOptions.HttpKeepAliveInterval = httpKeepAliveInterval;
|
||||||
|
_httpClient = CreateClient(newOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpClient CreateClient(ApiProxy? proxy, TimeSpan requestTimeout, TimeSpan? httpKeepAliveInterval)
|
private static HttpClient CreateClient(RestExchangeOptions options)
|
||||||
{
|
{
|
||||||
var handler = LibraryHelpers.CreateHttpClientMessageHandler(proxy, httpKeepAliveInterval);
|
var handler = LibraryHelpers.CreateHttpClientMessageHandler(options);
|
||||||
var client = new HttpClient(handler)
|
var client = new HttpClient(handler)
|
||||||
{
|
{
|
||||||
Timeout = requestTimeout
|
Timeout = options.RequestTimeout
|
||||||
};
|
};
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A token which a request can use to retrieve the next page if there are more pages in the result set
|
|
||||||
/// </summary>
|
|
||||||
public interface INextPageToken
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A datetime offset token
|
|
||||||
/// </summary>
|
|
||||||
public record DateTimeToken: INextPageToken
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Last result time
|
|
||||||
/// </summary>
|
|
||||||
public DateTime LastTime { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public DateTimeToken(DateTime timestamp)
|
|
||||||
{
|
|
||||||
LastTime = timestamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A current page index token
|
|
||||||
/// </summary>
|
|
||||||
public record PageToken: INextPageToken
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The next page index
|
|
||||||
/// </summary>
|
|
||||||
public int Page { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Page size
|
|
||||||
/// </summary>
|
|
||||||
public int PageSize { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public PageToken(int page, int pageSize)
|
|
||||||
{
|
|
||||||
Page = page;
|
|
||||||
PageSize = pageSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A id offset token
|
|
||||||
/// </summary>
|
|
||||||
public record FromIdToken : INextPageToken
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The last id from previous result
|
|
||||||
/// </summary>
|
|
||||||
public string FromToken { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public FromIdToken(string fromToken)
|
|
||||||
{
|
|
||||||
FromToken = fromToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A cursor token
|
|
||||||
/// </summary>
|
|
||||||
public record CursorToken : INextPageToken
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The next page cursor
|
|
||||||
/// </summary>
|
|
||||||
public string Cursor { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public CursorToken(string cursor)
|
|
||||||
{
|
|
||||||
Cursor = cursor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A result offset token
|
|
||||||
/// </summary>
|
|
||||||
public record OffsetToken : INextPageToken
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Offset in the result set
|
|
||||||
/// </summary>
|
|
||||||
public int Offset { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public OffsetToken(int offset)
|
|
||||||
{
|
|
||||||
Offset = offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get funding rate records
|
/// Get funding rate records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,14 +73,14 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spot get closed orders request options
|
/// Spot get closed orders request options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PaginatedEndpointOptions<GetClosedOrdersRequest> GetClosedFuturesOrdersOptions { get; }
|
GetClosedOrdersOptions GetClosedFuturesOrdersOptions { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get info on closed futures orders
|
/// Get info on closed futures orders
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedFuturesOrder[]>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedFuturesOrder[]>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Futures get order trades request options
|
/// Futures get order trades request options
|
||||||
@@ -96,14 +96,14 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Futures user trades request options
|
/// Futures user trades request options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PaginatedEndpointOptions<GetUserTradesRequest> GetFuturesUserTradesOptions { get; }
|
GetUserTradesOptions GetFuturesUserTradesOptions { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get futures user trade records
|
/// Get futures user trade records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedUserTrade[]>> GetFuturesUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedUserTrade[]>> GetFuturesUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Futures cancel order request options
|
/// Futures cancel order request options
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get index price kline/candlestick data
|
/// Get index price kline/candlestick data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedFuturesKline[]>> GetIndexPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedFuturesKline[]>> GetIndexPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get mark price kline/candlestick data
|
/// Get mark price kline/candlestick data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedFuturesKline[]>> GetMarkPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedFuturesKline[]>> GetMarkPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get position history
|
/// Get position history
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedPositionHistory[]>> GetPositionHistoryAsync(GetPositionHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedPositionHistory[]>> GetPositionHistoryAsync(GetPositionHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get deposit records
|
/// Get deposit records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<ExchangeWebResult<SharedDeposit[]>> GetDepositsAsync(GetDepositsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedDeposit[]>> GetDepositsAsync(GetDepositsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get kline/candlestick data
|
/// Get kline/candlestick data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<ExchangeWebResult<SharedKline[]>> GetKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedKline[]>> GetKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get public trade history
|
/// Get public trade history
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<ExchangeWebResult<SharedTrade[]>> GetTradeHistoryAsync(GetTradeHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedTrade[]>> GetTradeHistoryAsync(GetTradeHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Get withdrawal records
|
/// Get withdrawal records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<ExchangeWebResult<SharedWithdrawal[]>> GetWithdrawalsAsync(GetWithdrawalsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedWithdrawal[]>> GetWithdrawalsAsync(GetWithdrawalsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,14 +72,14 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spot get closed orders request options
|
/// Spot get closed orders request options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PaginatedEndpointOptions<GetClosedOrdersRequest> GetClosedSpotOrdersOptions { get; }
|
GetClosedOrdersOptions GetClosedSpotOrdersOptions { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get info on closed spot orders
|
/// Get info on closed spot orders
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedSpotOrder[]>> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedSpotOrder[]>> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spot get order trades request options
|
/// Spot get order trades request options
|
||||||
@@ -95,14 +95,14 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spot user trades request options
|
/// Spot user trades request options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PaginatedEndpointOptions<GetUserTradesRequest> GetSpotUserTradesOptions { get; }
|
GetUserTradesOptions GetSpotUserTradesOptions { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get spot user trade records
|
/// Get spot user trade records
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination request from the previous request result `NextPageRequest` property to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<SharedUserTrade[]>> GetSpotUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedUserTrade[]>> GetSpotUserTradesAsync(GetUserTradesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Spot cancel order request options
|
/// Spot cancel order request options
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Data.Common;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
namespace CryptoExchange.Net.SharedApis
|
||||||
@@ -99,6 +100,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
if (val == null)
|
if (val == null)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
|
if (val.Value is T typeVal)
|
||||||
|
return typeVal;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Type t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
Type t = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public TradingMode[]? DataTradeMode { get; }
|
public TradingMode[]? DataTradeMode { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Token to retrieve the next page with
|
/// Next page request, can be passed to the next request on the same endpoint to get the next page
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public INextPageToken? NextPageToken { get; }
|
public PageRequest? NextPageRequest { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -46,7 +46,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
string exchange,
|
string exchange,
|
||||||
TradingMode dataTradeMode,
|
TradingMode dataTradeMode,
|
||||||
WebCallResult<T> result,
|
WebCallResult<T> result,
|
||||||
INextPageToken? nextPageToken = null) :
|
PageRequest? nextPageToken = null) :
|
||||||
base(result.ResponseStatusCode,
|
base(result.ResponseStatusCode,
|
||||||
result.HttpVersion,
|
result.HttpVersion,
|
||||||
result.ResponseHeaders,
|
result.ResponseHeaders,
|
||||||
@@ -64,7 +64,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
{
|
{
|
||||||
DataTradeMode = new[] { dataTradeMode };
|
DataTradeMode = new[] { dataTradeMode };
|
||||||
Exchange = exchange;
|
Exchange = exchange;
|
||||||
NextPageToken = nextPageToken;
|
NextPageRequest = nextPageToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -74,7 +74,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
string exchange,
|
string exchange,
|
||||||
TradingMode[]? dataTradeModes,
|
TradingMode[]? dataTradeModes,
|
||||||
WebCallResult<T> result,
|
WebCallResult<T> result,
|
||||||
INextPageToken? nextPageToken = null) :
|
PageRequest? nextPageRequest = null) :
|
||||||
base(result.ResponseStatusCode,
|
base(result.ResponseStatusCode,
|
||||||
result.HttpVersion,
|
result.HttpVersion,
|
||||||
result.ResponseHeaders,
|
result.ResponseHeaders,
|
||||||
@@ -92,7 +92,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
{
|
{
|
||||||
DataTradeMode = dataTradeModes;
|
DataTradeMode = dataTradeModes;
|
||||||
Exchange = exchange;
|
Exchange = exchange;
|
||||||
NextPageToken = nextPageToken;
|
NextPageRequest = nextPageRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -115,7 +115,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
ResultDataSource dataSource,
|
ResultDataSource dataSource,
|
||||||
[AllowNull] T data,
|
[AllowNull] T data,
|
||||||
Error? error,
|
Error? error,
|
||||||
INextPageToken? nextPageToken = null) : base(
|
PageRequest? nextPageToken = null) : base(
|
||||||
code,
|
code,
|
||||||
httpVersion,
|
httpVersion,
|
||||||
responseHeaders,
|
responseHeaders,
|
||||||
@@ -133,7 +133,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
{
|
{
|
||||||
DataTradeMode = dataTradeModes;
|
DataTradeMode = dataTradeModes;
|
||||||
Exchange = exchange;
|
Exchange = exchange;
|
||||||
NextPageToken = nextPageToken;
|
NextPageRequest = nextPageToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -144,7 +144,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new ExchangeWebResult<K> As<K>([AllowNull] K data)
|
public new ExchangeWebResult<K> As<K>([AllowNull] K data)
|
||||||
{
|
{
|
||||||
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageToken);
|
return new ExchangeWebResult<K>(Exchange, DataTradeMode, ResponseStatusCode, HttpVersion, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error, NextPageRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
namespace CryptoExchange.Net.SharedApis
|
||||||
@@ -8,24 +9,36 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetClosedOrdersOptions : PaginatedEndpointOptions<GetClosedOrdersRequest>
|
public class GetClosedOrdersOptions : PaginatedEndpointOptions<GetClosedOrdersRequest>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Whether the start/end time filter is supported
|
|
||||||
/// </summary>
|
|
||||||
public bool TimeFilterSupported { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetClosedOrdersOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
public GetClosedOrdersOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
||||||
|
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
||||||
{
|
{
|
||||||
TimeFilterSupported = timeFilterSupported;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
public override Error? ValidateRequest(string exchange, GetClosedOrdersRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||||
{
|
{
|
||||||
if (!TimeFilterSupported && request.StartTime != null)
|
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||||
return ArgumentError.Invalid(nameof(GetClosedOrdersRequest.StartTime), $"Time filter is not supported");
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||||
|
|
||||||
|
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||||
|
|
||||||
|
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||||
|
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||||
|
|
||||||
|
if (!TimePeriodFilterSupport)
|
||||||
|
{
|
||||||
|
// When going descending we can still allow startTime filter to limit the results
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if ((request.Direction != DataDirection.Descending && request.StartTime != null)
|
||||||
|
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||||
|
{
|
||||||
|
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
}
|
}
|
||||||
@@ -34,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public override string ToString(string exchange)
|
public override string ToString(string exchange)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(base.ToString(exchange));
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
sb.AppendLine($"Time filter supported: {TimeFilterSupported}");
|
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
namespace CryptoExchange.Net.SharedApis
|
||||||
@@ -8,24 +9,36 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetDepositsOptions : PaginatedEndpointOptions<GetDepositsRequest>
|
public class GetDepositsOptions : PaginatedEndpointOptions<GetDepositsRequest>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Whether the start/end time filter is supported
|
|
||||||
/// </summary>
|
|
||||||
public bool TimeFilterSupported { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetDepositsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
public GetDepositsOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
||||||
|
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
||||||
{
|
{
|
||||||
TimeFilterSupported = timeFilterSupported;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
public override Error? ValidateRequest(string exchange, GetDepositsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||||
{
|
{
|
||||||
if (!TimeFilterSupported && request.StartTime != null)
|
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||||
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||||
|
|
||||||
|
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||||
|
|
||||||
|
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||||
|
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||||
|
|
||||||
|
if (!TimePeriodFilterSupport)
|
||||||
|
{
|
||||||
|
// When going descending we can still allow startTime filter to limit the results
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if ((request.Direction != DataDirection.Descending && request.StartTime != null)
|
||||||
|
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||||
|
{
|
||||||
|
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
}
|
}
|
||||||
@@ -34,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public override string ToString(string exchange)
|
public override string ToString(string exchange)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(base.ToString(exchange));
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
sb.AppendLine($"Time filter supported: {TimeFilterSupported}");
|
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-2
@@ -1,4 +1,8 @@
|
|||||||
namespace CryptoExchange.Net.SharedApis
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options for requesting funding rate history
|
/// Options for requesting funding rate history
|
||||||
@@ -8,8 +12,43 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetFundingRateHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
public GetFundingRateHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication)
|
||||||
|
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Error? ValidateRequest(string exchange, GetFundingRateHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||||
|
{
|
||||||
|
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||||
|
|
||||||
|
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||||
|
|
||||||
|
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||||
|
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||||
|
|
||||||
|
if (!TimePeriodFilterSupport)
|
||||||
|
{
|
||||||
|
// When going descending we can still allow startTime filter to limit the results
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if ((request.Direction != DataDirection.Descending && request.StartTime != null)
|
||||||
|
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||||
|
{
|
||||||
|
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString(string exchange)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
|
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,12 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of data points which can be requested
|
/// Max number of data points which can be requested
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? MaxTotalDataPoints { get; set; }
|
public int? MaxTotalDataPoints { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// The max age of the data that can be requested
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan? MaxAge { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
public GetKlinesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication)
|
||||||
|
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication)
|
||||||
{
|
{
|
||||||
SupportIntervals = new[]
|
SupportIntervals = new[]
|
||||||
{
|
{
|
||||||
@@ -50,7 +47,8 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
public GetKlinesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals)
|
||||||
|
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication)
|
||||||
{
|
{
|
||||||
SupportIntervals = intervals;
|
SupportIntervals = intervals;
|
||||||
}
|
}
|
||||||
@@ -68,12 +66,29 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
if (!IsSupported(request.Interval))
|
if (!IsSupported(request.Interval))
|
||||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), "Interval not supported");
|
return ArgumentError.Invalid(nameof(GetKlinesRequest.Interval), "Interval not supported");
|
||||||
|
|
||||||
|
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||||
|
|
||||||
|
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||||
|
|
||||||
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available");
|
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} klines are available");
|
||||||
|
|
||||||
if (request.Limit > MaxLimit)
|
if (request.Limit > MaxLimit)
|
||||||
return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only {MaxLimit} klines can be retrieved per request");
|
return ArgumentError.Invalid(nameof(GetKlinesRequest.Limit), $"Only {MaxLimit} klines can be retrieved per request");
|
||||||
|
|
||||||
|
if (!TimePeriodFilterSupport)
|
||||||
|
{
|
||||||
|
// When going descending we can still allow startTime filter to limit the results
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if ((request.Direction == DataDirection.Ascending && request.StartTime != null)
|
||||||
|
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||||
|
{
|
||||||
|
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (MaxTotalDataPoints.HasValue)
|
if (MaxTotalDataPoints.HasValue)
|
||||||
{
|
{
|
||||||
if (request.Limit > MaxTotalDataPoints.Value)
|
if (request.Limit > MaxTotalDataPoints.Value)
|
||||||
@@ -93,6 +108,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public override string ToString(string exchange)
|
public override string ToString(string exchange)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(base.ToString(exchange));
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
|
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||||
sb.AppendLine($"Supported SharedKlineInterval values: {string.Join(", ", SupportIntervals)}");
|
sb.AppendLine($"Supported SharedKlineInterval values: {string.Join(", ", SupportIntervals)}");
|
||||||
if (MaxAge != null)
|
if (MaxAge != null)
|
||||||
sb.AppendLine($"Max age of data: {MaxAge}");
|
sb.AppendLine($"Max age of data: {MaxAge}");
|
||||||
|
|||||||
+41
-2
@@ -1,4 +1,8 @@
|
|||||||
namespace CryptoExchange.Net.SharedApis
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Options for requesting position history
|
/// Options for requesting position history
|
||||||
@@ -8,8 +12,43 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetPositionHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
public GetPositionHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
||||||
|
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Error? ValidateRequest(string exchange, GetPositionHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||||
|
{
|
||||||
|
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||||
|
|
||||||
|
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||||
|
|
||||||
|
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||||
|
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||||
|
|
||||||
|
if (!TimePeriodFilterSupport)
|
||||||
|
{
|
||||||
|
// When going descending we can still allow startTime filter to limit the results
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if ((request.Direction != DataDirection.Descending && request.StartTime != null)
|
||||||
|
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||||
|
{
|
||||||
|
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString(string exchange)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
|
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,34 +9,27 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetTradeHistoryOptions : PaginatedEndpointOptions<GetTradeHistoryRequest>
|
public class GetTradeHistoryOptions : PaginatedEndpointOptions<GetTradeHistoryRequest>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// The max age of data that can be requested
|
|
||||||
/// </summary>
|
|
||||||
public TimeSpan? MaxAge { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetTradeHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
public GetTradeHistoryOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit, bool needsAuthentication)
|
||||||
|
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, needsAuthentication)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
public override Error? ValidateRequest(string exchange, GetTradeHistoryRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||||
{
|
{
|
||||||
|
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||||
|
|
||||||
|
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||||
|
|
||||||
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||||
return ArgumentError.Invalid(nameof(GetTradeHistoryRequest.StartTime), $"Only the most recent {MaxAge} trades are available");
|
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||||
|
|
||||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string ToString(string exchange)
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder(base.ToString(exchange));
|
|
||||||
if (MaxAge != null)
|
|
||||||
sb.AppendLine($"Max age of data: {MaxAge}");
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Options for requesting user trades
|
||||||
|
/// </summary>
|
||||||
|
public class GetUserTradesOptions : PaginatedEndpointOptions<GetUserTradesRequest>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public GetUserTradesOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
||||||
|
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Error? ValidateRequest(string exchange, GetUserTradesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||||
|
{
|
||||||
|
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||||
|
|
||||||
|
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||||
|
|
||||||
|
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||||
|
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||||
|
|
||||||
|
if (!TimePeriodFilterSupport)
|
||||||
|
{
|
||||||
|
// When going descending we can still allow startTime filter to limit the results
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if ((request.Direction != DataDirection.Descending && request.StartTime != null)
|
||||||
|
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||||
|
{
|
||||||
|
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString(string exchange)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
|
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
namespace CryptoExchange.Net.SharedApis
|
||||||
@@ -8,24 +9,36 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetWithdrawalsOptions : PaginatedEndpointOptions<GetWithdrawalsRequest>
|
public class GetWithdrawalsOptions : PaginatedEndpointOptions<GetWithdrawalsRequest>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Whether the start/end time filter is supported
|
|
||||||
/// </summary>
|
|
||||||
public bool TimeFilterSupported { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GetWithdrawalsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
public GetWithdrawalsOptions(bool supportsAscending, bool supportsDescending, bool timeFilterSupported, int maxLimit)
|
||||||
|
: base(supportsAscending, supportsDescending, timeFilterSupported, maxLimit, true)
|
||||||
{
|
{
|
||||||
TimeFilterSupported = timeFilterSupported;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
public override Error? ValidateRequest(string exchange, GetWithdrawalsRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
|
||||||
{
|
{
|
||||||
if (!TimeFilterSupported && request.StartTime != null)
|
if (!SupportsAscending && request.Direction == DataDirection.Ascending)
|
||||||
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.StartTime), $"Time filter is not supported");
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Ascending direction is not supported");
|
||||||
|
|
||||||
|
if (!SupportsDescending && request.Direction == DataDirection.Descending)
|
||||||
|
return ArgumentError.Invalid(nameof(GetWithdrawalsRequest.Direction), $"Descending direction is not supported");
|
||||||
|
|
||||||
|
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||||
|
return ArgumentError.Invalid(nameof(GetKlinesRequest.StartTime), $"Only the most recent {MaxAge} period data is available");
|
||||||
|
|
||||||
|
if (!TimePeriodFilterSupport)
|
||||||
|
{
|
||||||
|
// When going descending we can still allow startTime filter to limit the results
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if ((request.Direction != DataDirection.Descending && request.StartTime != null)
|
||||||
|
|| (request.EndTime != null && now - request.EndTime > TimeSpan.FromSeconds(5)))
|
||||||
|
{
|
||||||
|
return ArgumentError.Invalid(nameof(GetDepositsRequest.StartTime), $"Time filter is not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
|
||||||
}
|
}
|
||||||
@@ -34,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public override string ToString(string exchange)
|
public override string ToString(string exchange)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(base.ToString(exchange));
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
sb.AppendLine($"Time filter supported: {TimeFilterSupported}");
|
sb.AppendLine($"Time filter supported: {TimePeriodFilterSupport}");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.SharedApis
|
namespace CryptoExchange.Net.SharedApis
|
||||||
@@ -14,9 +15,13 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Type of pagination supported
|
/// Whether ascending data retrieval and pagination is available
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SharedPaginationSupport PaginationSupport { get; }
|
public bool SupportsAscending { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Whether ascending data retrieval and pagination is available
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsDescending { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether filtering based on start/end time is supported
|
/// Whether filtering based on start/end time is supported
|
||||||
@@ -28,12 +33,23 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaxLimit { get; set; }
|
public int MaxLimit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Max age of data that can be requested
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? MaxAge { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool timePeriodSupport, int maxLimit, bool needsAuthentication) : base(needsAuthentication)
|
public PaginatedEndpointOptions(
|
||||||
|
bool supportsAscending,
|
||||||
|
bool supportsDescending,
|
||||||
|
bool timePeriodSupport,
|
||||||
|
int maxLimit,
|
||||||
|
bool needsAuthentication) : base(needsAuthentication)
|
||||||
{
|
{
|
||||||
PaginationSupport = paginationType;
|
SupportsAscending = supportsAscending;
|
||||||
|
SupportsDescending = supportsDescending;
|
||||||
TimePeriodFilterSupport = timePeriodSupport;
|
TimePeriodFilterSupport = timePeriodSupport;
|
||||||
MaxLimit = maxLimit;
|
MaxLimit = maxLimit;
|
||||||
}
|
}
|
||||||
@@ -42,9 +58,11 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
public override string ToString(string exchange)
|
public override string ToString(string exchange)
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder(base.ToString(exchange));
|
var sb = new StringBuilder(base.ToString(exchange));
|
||||||
sb.AppendLine($"Pagination type: {PaginationSupport}");
|
sb.AppendLine($"Ascending retrieval supported: {SupportsAscending}");
|
||||||
|
sb.AppendLine($"Descending retrieval supported: {SupportsDescending}");
|
||||||
sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}");
|
sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}");
|
||||||
sb.AppendLine($"Max limit: {MaxLimit}");
|
sb.AppendLine($"Max limit: {MaxLimit}");
|
||||||
|
sb.AppendLine($"Max age: {MaxAge}");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Data direction
|
||||||
|
/// </summary>
|
||||||
|
public enum DataDirection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Old to new order
|
||||||
|
/// </summary>
|
||||||
|
Ascending,
|
||||||
|
/// <summary>
|
||||||
|
/// New to old order
|
||||||
|
/// </summary>
|
||||||
|
Descending
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Next page request info
|
||||||
|
/// </summary>
|
||||||
|
public class PageRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pagination cursor
|
||||||
|
/// </summary>
|
||||||
|
public string? Cursor { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Page number
|
||||||
|
/// </summary>
|
||||||
|
public int? Page { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Result offset
|
||||||
|
/// </summary>
|
||||||
|
public int? Offset { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// From id filter
|
||||||
|
/// </summary>
|
||||||
|
public string? FromId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Start time filter
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartTime { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// End time filter
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndTime { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pagination methods
|
||||||
|
/// </summary>
|
||||||
|
public static class Pagination
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get pagination parameters
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="direction">The data direction</param>
|
||||||
|
/// <param name="limit">Result limit</param>
|
||||||
|
/// <param name="requestStartTime">User request start time</param>
|
||||||
|
/// <param name="requestEndTime">User request end time</param>
|
||||||
|
/// <param name="paginationRequest">Provided page request</param>
|
||||||
|
/// <param name="setOtherTimeLimiter">Whether to set start time if direction is descending, or end time if direction is ascending</param>
|
||||||
|
/// <param name="maxPeriod">Max period the time filters can span</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static PaginationParameters GetPaginationParameters(
|
||||||
|
DataDirection direction,
|
||||||
|
int limit,
|
||||||
|
DateTime? requestStartTime,
|
||||||
|
DateTime requestEndTime,
|
||||||
|
PageRequest? paginationRequest,
|
||||||
|
bool setOtherTimeLimiter = true,
|
||||||
|
TimeSpan? maxPeriod = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var startTime = paginationRequest?.StartTime ?? requestStartTime;
|
||||||
|
var endTime = paginationRequest?.EndTime ?? requestEndTime;
|
||||||
|
if (maxPeriod != null)
|
||||||
|
{
|
||||||
|
if (direction == DataDirection.Ascending)
|
||||||
|
{
|
||||||
|
if (startTime == null)
|
||||||
|
{
|
||||||
|
startTime = endTime.Add(-maxPeriod.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
endTime = startTime.Value.Add(maxPeriod.Value);
|
||||||
|
if (endTime > DateTime.UtcNow)
|
||||||
|
endTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
startTime = endTime.Add(-maxPeriod.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PaginationParameters
|
||||||
|
{
|
||||||
|
Limit = limit,
|
||||||
|
StartTime = direction == DataDirection.Ascending || setOtherTimeLimiter ? startTime : null,
|
||||||
|
EndTime = direction == DataDirection.Descending || setOtherTimeLimiter ? endTime : null,
|
||||||
|
Direction = direction,
|
||||||
|
FromId = paginationRequest?.FromId,
|
||||||
|
Offset = paginationRequest?.Offset,
|
||||||
|
Page = paginationRequest?.Page,
|
||||||
|
Cursor = paginationRequest?.Cursor
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the next page request parameters from result kline data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nextPageRequest">Callback for returning the next page request</param>
|
||||||
|
/// <param name="resultCount">Number of results in data</param>
|
||||||
|
/// <param name="timestamps">Timestamps of the result data</param>
|
||||||
|
/// <param name="requestStartTime">User request start time</param>
|
||||||
|
/// <param name="requestEndTime">User request end time</param>
|
||||||
|
/// <param name="lastPaginationData">The last used pagination data</param>
|
||||||
|
/// <param name="interval">Kline interval</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static PageRequest? GetNextPageRequestKlines(
|
||||||
|
Func<PageRequest?> nextPageRequest,
|
||||||
|
int resultCount,
|
||||||
|
IEnumerable<DateTime> timestamps,
|
||||||
|
DateTime? requestStartTime,
|
||||||
|
DateTime requestEndTime,
|
||||||
|
PaginationParameters lastPaginationData,
|
||||||
|
SharedKlineInterval interval
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (HasNextPageKlines(resultCount, timestamps, requestStartTime, requestEndTime, lastPaginationData.Limit, lastPaginationData.Direction, interval))
|
||||||
|
{
|
||||||
|
var result = nextPageRequest();
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
result.StartTime ??= lastPaginationData.StartTime;
|
||||||
|
result.EndTime ??= lastPaginationData.EndTime;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the next page request parameters from result data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nextPageRequest">Callback for returning the next page request</param>
|
||||||
|
/// <param name="resultCount">Number of results in data</param>
|
||||||
|
/// <param name="timestamps">Timestamps of the result data</param>
|
||||||
|
/// <param name="requestStartTime">User request start time</param>
|
||||||
|
/// <param name="requestEndTime">User request end time</param>
|
||||||
|
/// <param name="lastPaginationData">The last used pagination data</param>
|
||||||
|
/// <param name="maxPeriod">Max period the time filters can span</param>
|
||||||
|
/// <param name="maxAge">Max age of the data</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static PageRequest? GetNextPageRequest(
|
||||||
|
Func<PageRequest?> nextPageRequest,
|
||||||
|
int resultCount,
|
||||||
|
IEnumerable<DateTime> timestamps,
|
||||||
|
DateTime? requestStartTime,
|
||||||
|
DateTime requestEndTime,
|
||||||
|
PaginationParameters lastPaginationData,
|
||||||
|
TimeSpan? maxPeriod = null,
|
||||||
|
TimeSpan? maxAge = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (HasNextPage(resultCount, timestamps, requestStartTime, requestEndTime, lastPaginationData.Limit, lastPaginationData.Direction))
|
||||||
|
{
|
||||||
|
var result = nextPageRequest();
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
result.StartTime ??= lastPaginationData.StartTime;
|
||||||
|
result.EndTime ??= lastPaginationData.EndTime;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxPeriod != null)
|
||||||
|
{
|
||||||
|
if (HasNextPeriod(requestStartTime, requestEndTime, lastPaginationData.Direction, lastPaginationData, maxPeriod.Value, maxAge))
|
||||||
|
{
|
||||||
|
var (startTime, endTime) = GetNextPeriod(requestStartTime, requestEndTime, lastPaginationData.Direction, lastPaginationData, maxPeriod.Value, maxAge);
|
||||||
|
return new PageRequest
|
||||||
|
{
|
||||||
|
StartTime = startTime,
|
||||||
|
EndTime = endTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether there is (potentially) another page available
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resultCount">Number of result entries</param>
|
||||||
|
/// <param name="timestamps">Timestamps</param>
|
||||||
|
/// <param name="requestStartTime">User request start time</param>
|
||||||
|
/// <param name="requestEndTime">User request end time</param>
|
||||||
|
/// <param name="limit">Max number of results requested</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
|
/// <param name="interval">Kline interval</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool HasNextPageKlines(
|
||||||
|
int resultCount,
|
||||||
|
IEnumerable<DateTime> timestamps,
|
||||||
|
DateTime? requestStartTime,
|
||||||
|
DateTime requestEndTime,
|
||||||
|
int limit,
|
||||||
|
DataDirection direction,
|
||||||
|
SharedKlineInterval interval
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (resultCount < limit)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (direction == DataDirection.Ascending)
|
||||||
|
{
|
||||||
|
if (timestamps.Max().AddSeconds((int)interval) >= requestEndTime)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (timestamps.Min().AddSeconds((int)interval) < requestStartTime)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether there is (potentially) another page available
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resultCount">Number of result entries</param>
|
||||||
|
/// <param name="timestamps">Timestamps</param>
|
||||||
|
/// <param name="requestStartTime">User request start time</param>
|
||||||
|
/// <param name="requestEndTime">User request end time</param>
|
||||||
|
/// <param name="limit">Max number of results requested</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool HasNextPage(
|
||||||
|
int resultCount,
|
||||||
|
IEnumerable<DateTime> timestamps,
|
||||||
|
DateTime? requestStartTime,
|
||||||
|
DateTime requestEndTime,
|
||||||
|
int limit,
|
||||||
|
DataDirection direction)
|
||||||
|
{
|
||||||
|
if (resultCount < limit)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!timestamps.Any())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (direction == DataDirection.Ascending)
|
||||||
|
{
|
||||||
|
if (timestamps.Max() >= requestEndTime)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (timestamps.Min() < requestStartTime)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the next page PageRequest
|
||||||
|
/// </summary>
|
||||||
|
public static PageRequest NextPageFromPage(PaginationParameters lastPaginationData)
|
||||||
|
{
|
||||||
|
return new PageRequest { Page = (lastPaginationData.Page ?? 1) + 1 };
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Get the next offset PageRequest
|
||||||
|
/// </summary>
|
||||||
|
public static PageRequest NextPageFromOffset(PaginationParameters lastPaginationData, int resultCount)
|
||||||
|
{
|
||||||
|
return new PageRequest { Offset = (lastPaginationData.Offset ?? 0) + resultCount };
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Get the next page cursor PageRequest
|
||||||
|
/// </summary>
|
||||||
|
public static PageRequest NextPageFromCursor(string nextCursor)
|
||||||
|
{
|
||||||
|
return new PageRequest { Cursor = nextCursor };
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Get the next id PageRequest
|
||||||
|
/// </summary>
|
||||||
|
public static PageRequest NextPageFromId(long nextFromId)
|
||||||
|
{
|
||||||
|
return new PageRequest { FromId = nextFromId.ToString() };
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Get the next id PageRequest
|
||||||
|
/// </summary>
|
||||||
|
public static PageRequest NextPageFromId(string nextFromId)
|
||||||
|
{
|
||||||
|
return new PageRequest { FromId = nextFromId };
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Get the next start/end time PageRequest
|
||||||
|
/// </summary>
|
||||||
|
public static PageRequest NextPageFromTime(PaginationParameters lastPaginationData, DateTime lastTimestamp, bool setOtherTimeLimiter = true)
|
||||||
|
{
|
||||||
|
if (lastPaginationData.Direction == DataDirection.Ascending)
|
||||||
|
return new PageRequest { StartTime = lastTimestamp.AddMilliseconds(1), EndTime = setOtherTimeLimiter ? lastPaginationData.EndTime : null };
|
||||||
|
else
|
||||||
|
return new PageRequest { EndTime = lastTimestamp.AddMilliseconds(-1), StartTime = setOtherTimeLimiter ? lastPaginationData.StartTime : null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the next start/end time klines PageRequest
|
||||||
|
/// </summary>
|
||||||
|
public static PageRequest NextPageFromTimeKlines(DataDirection direction, GetKlinesRequest request, DateTime lastTimestamp, int limit)
|
||||||
|
{
|
||||||
|
if (direction == DataDirection.Ascending)
|
||||||
|
{
|
||||||
|
var nextStartTime = lastTimestamp.AddSeconds((int)request.Interval);
|
||||||
|
var endTime = nextStartTime.AddSeconds(limit * (int)request.Interval);
|
||||||
|
var requestEndTime = request.EndTime ?? DateTime.UtcNow;
|
||||||
|
if (endTime > requestEndTime)
|
||||||
|
endTime = requestEndTime;
|
||||||
|
|
||||||
|
return new PageRequest { StartTime = nextStartTime, EndTime = endTime };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var nextEndTime = lastTimestamp.AddSeconds(-(int)request.Interval);
|
||||||
|
var startTime = nextEndTime.AddSeconds(-(limit * (int)request.Interval));
|
||||||
|
var requestStartTime = request.StartTime ?? DateTime.UtcNow;
|
||||||
|
if (startTime < requestStartTime)
|
||||||
|
startTime = requestStartTime;
|
||||||
|
return new PageRequest { StartTime = startTime, EndTime = nextEndTime };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether another time period is to be requested
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestStartTime">User request start time</param>
|
||||||
|
/// <param name="requestEndTime">User request end time</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
|
/// <param name="lastPaginationParameters">Pagination parameters used</param>
|
||||||
|
/// <param name="period">Max time period a request can span</param>
|
||||||
|
/// <param name="maxAge">Max age of data that can be requested</param>
|
||||||
|
public static bool HasNextPeriod(
|
||||||
|
DateTime? requestStartTime,
|
||||||
|
DateTime requestEndTime,
|
||||||
|
DataDirection direction,
|
||||||
|
PaginationParameters lastPaginationParameters,
|
||||||
|
TimeSpan period,
|
||||||
|
TimeSpan? maxAge)
|
||||||
|
{
|
||||||
|
if (direction == DataDirection.Ascending && lastPaginationParameters.StartTime == null)
|
||||||
|
throw new InvalidOperationException("Invalid pagination data; no start time for ascending pagination");
|
||||||
|
|
||||||
|
if (direction == DataDirection.Ascending)
|
||||||
|
{
|
||||||
|
return (requestEndTime - lastPaginationParameters.EndTime!.Value).TotalSeconds > 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var lastPageStartTime = lastPaginationParameters.StartTime ?? lastPaginationParameters.EndTime!.Value.Add(-period);
|
||||||
|
if (requestStartTime != null)
|
||||||
|
{
|
||||||
|
var nextPeriodDuration = lastPageStartTime - requestStartTime.Value;
|
||||||
|
return nextPeriodDuration.TotalSeconds > 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var nextStartTime = lastPageStartTime - period;
|
||||||
|
if (maxAge != null)
|
||||||
|
{
|
||||||
|
var minStartTime = DateTime.UtcNow - maxAge.Value;
|
||||||
|
if ((nextStartTime.Add(period) - minStartTime).TotalSeconds < 1)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextPeriodDuration = lastPageStartTime - nextStartTime;
|
||||||
|
return (nextPeriodDuration).TotalSeconds > 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the start/end time for the next data period
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestStartTime">User request start time</param>
|
||||||
|
/// <param name="requestEndTime">User request end time</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
|
/// <param name="lastPaginationParameters">Pagination parameters used</param>
|
||||||
|
/// <param name="period">Max time period a request can span</param>
|
||||||
|
/// <param name="maxAge">Max age of data that can be requested</param>
|
||||||
|
public static (DateTime? startTime, DateTime? endTime) GetNextPeriod(
|
||||||
|
DateTime? requestStartTime,
|
||||||
|
DateTime requestEndTime,
|
||||||
|
DataDirection direction,
|
||||||
|
PaginationParameters lastPaginationParameters,
|
||||||
|
TimeSpan period,
|
||||||
|
TimeSpan? maxAge
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DateTime? nextStartTime = null;
|
||||||
|
DateTime? nextEndTime = null;
|
||||||
|
if (direction == DataDirection.Ascending)
|
||||||
|
{
|
||||||
|
if (lastPaginationParameters.StartTime != null)
|
||||||
|
nextStartTime = lastPaginationParameters.StartTime.Value.Add(period);
|
||||||
|
if (lastPaginationParameters.EndTime != null)
|
||||||
|
nextEndTime = lastPaginationParameters.EndTime.Value.Add(period);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (lastPaginationParameters.StartTime != null)
|
||||||
|
nextStartTime = lastPaginationParameters.StartTime.Value.Add(-period);
|
||||||
|
if (lastPaginationParameters.EndTime != null)
|
||||||
|
nextEndTime = lastPaginationParameters.EndTime.Value.Add(-period);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextStartTime != null && nextStartTime < requestStartTime)
|
||||||
|
nextStartTime = requestStartTime;
|
||||||
|
|
||||||
|
if (nextStartTime != null && maxAge != null && nextStartTime < DateTime.UtcNow - maxAge)
|
||||||
|
{
|
||||||
|
nextStartTime = DateTime.UtcNow.Add(-maxAge.Value);
|
||||||
|
// Add 30 seconds to max sure the client/server time offset and latency doesn't push the timestamp over the limit
|
||||||
|
nextStartTime = nextStartTime.Value.Add(TimeSpan.FromSeconds(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextEndTime != null && nextEndTime > requestEndTime)
|
||||||
|
nextEndTime = requestEndTime;
|
||||||
|
|
||||||
|
return (nextStartTime, nextEndTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pagination parameters
|
||||||
|
/// </summary>
|
||||||
|
public record PaginationParameters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Data direction
|
||||||
|
/// </summary>
|
||||||
|
public DataDirection Direction { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Start time filter
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartTime { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// End time filter
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndTime { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Id filter
|
||||||
|
/// </summary>
|
||||||
|
public string? FromId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Result offset
|
||||||
|
/// </summary>
|
||||||
|
public int? Offset { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Page number
|
||||||
|
/// </summary>
|
||||||
|
public int? Page { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Pagination cursor
|
||||||
|
/// </summary>
|
||||||
|
public string? Cursor { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Max number of results
|
||||||
|
/// </summary>
|
||||||
|
public int Limit { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; }
|
public int? Limit { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Data direction
|
||||||
|
/// </summary>
|
||||||
|
public DataDirection? Direction { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetClosedOrdersRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
public GetClosedOrdersRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||||
{
|
{
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
|
Direction = direction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; }
|
public int? Limit { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Data direction
|
||||||
|
/// </summary>
|
||||||
|
public DataDirection? Direction { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -31,13 +35,15 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetDepositsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
public GetDepositsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||||
{
|
{
|
||||||
Asset = asset;
|
Asset = asset;
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
|
Direction = direction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; set; }
|
public int? Limit { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Data direction
|
||||||
|
/// </summary>
|
||||||
|
public DataDirection? Direction { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetFundingRateHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
public GetFundingRateHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||||
{
|
{
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
|
Direction = direction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; set; }
|
public int? Limit { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Data direction
|
||||||
|
/// </summary>
|
||||||
|
public DataDirection? Direction { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -32,13 +36,15 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetKlinesRequest(SharedSymbol symbol, SharedKlineInterval interval, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
public GetKlinesRequest(SharedSymbol symbol, SharedKlineInterval interval, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||||
{
|
{
|
||||||
Interval = interval;
|
Interval = interval;
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
|
Direction = direction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; set; }
|
public int? Limit { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Data direction
|
||||||
|
/// </summary>
|
||||||
|
public DataDirection? Direction { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -35,13 +39,15 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetPositionHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
public GetPositionHistoryRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||||
{
|
{
|
||||||
Symbol = symbol;
|
Symbol = symbol;
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
|
Direction = direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -51,13 +57,15 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetPositionHistoryRequest(TradingMode? tradeMode = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
public GetPositionHistoryRequest(TradingMode? tradeMode = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||||
{
|
{
|
||||||
TradingMode = tradeMode;
|
TradingMode = tradeMode;
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
|
Direction = direction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,19 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Filter by start time
|
/// Filter by start time
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime StartTime { get; }
|
public DateTime StartTime { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Filter by end time
|
/// Filter by end time
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime EndTime { get; }
|
public DateTime? EndTime { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; }
|
public int? Limit { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Data direction
|
||||||
|
/// </summary>
|
||||||
|
public DataDirection? Direction { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetTradeHistoryRequest(SharedSymbol symbol, DateTime startTime, DateTime endTime, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
public GetTradeHistoryRequest(SharedSymbol symbol, DateTime startTime, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||||
{
|
{
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
|
Direction = direction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; }
|
public int? Limit { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Data direction
|
||||||
|
/// </summary>
|
||||||
|
public DataDirection? Direction { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -27,12 +31,14 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetUserTradesRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
public GetUserTradesRequest(SharedSymbol symbol, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||||
{
|
{
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
|
Direction = direction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// Max number of results
|
/// Max number of results
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? Limit { get; }
|
public int? Limit { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Data direction
|
||||||
|
/// </summary>
|
||||||
|
public DataDirection? Direction { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -31,13 +35,15 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="startTime">Filter by start time</param>
|
/// <param name="startTime">Filter by start time</param>
|
||||||
/// <param name="endTime">Filter by end time</param>
|
/// <param name="endTime">Filter by end time</param>
|
||||||
/// <param name="limit">Max number of results</param>
|
/// <param name="limit">Max number of results</param>
|
||||||
|
/// <param name="direction">Data direction</param>
|
||||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||||
public GetWithdrawalsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
public GetWithdrawalsRequest(string? asset = null, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, DataDirection? direction = null, ExchangeParameters? exchangeParameters = null) : base(exchangeParameters)
|
||||||
{
|
{
|
||||||
Asset = asset;
|
Asset = asset;
|
||||||
StartTime = startTime;
|
StartTime = startTime;
|
||||||
EndTime = endTime;
|
EndTime = endTime;
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
|
Direction = direction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,8 +123,15 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
return _listeners.OfType<Subscription>().Count(h => h.UserSubscription);
|
return _listeners.OfType<Subscription>().Count(h => h.UserSubscription);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,8 +142,16 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
return _listeners.OfType<Subscription>().Where(h => h.UserSubscription).ToArray();
|
return _listeners.OfType<Subscription>().Where(h => h.UserSubscription).ToArray();
|
||||||
|
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,8 +255,15 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
return _listeners.OfType<Subscription>().Select(x => x.Topic).Where(t => t != null).ToArray()!;
|
return _listeners.OfType<Subscription>().Select(x => x.Topic).Where(t => t != null).ToArray()!;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,18 +274,21 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
return _listeners.OfType<Query>().Where(x => !x.Completed).Count();
|
return _listeners.OfType<Query>().Where(x => !x.Completed).Count();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private bool _pausedActivity;
|
private bool _pausedActivity;
|
||||||
#if NET9_0_OR_GREATER
|
private readonly ReaderWriterLockSlim _listenersLock = new ReaderWriterLockSlim();
|
||||||
private readonly Lock _listenersLock = new Lock();
|
|
||||||
#else
|
|
||||||
private readonly object _listenersLock = new object();
|
|
||||||
#endif
|
|
||||||
private readonly List<IMessageProcessor> _listeners;
|
private readonly List<IMessageProcessor> _listeners;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private SocketStatus _status;
|
private SocketStatus _status;
|
||||||
@@ -340,8 +365,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (ApiClient._socketConnections.ContainsKey(SocketId))
|
if (ApiClient._socketConnections.ContainsKey(SocketId))
|
||||||
ApiClient._socketConnections.TryRemove(SocketId, out _);
|
ApiClient._socketConnections.TryRemove(SocketId, out _);
|
||||||
|
|
||||||
lock (_listenersLock)
|
try
|
||||||
{
|
{
|
||||||
|
_listenersLock.EnterWriteLock();
|
||||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription && !l.IsClosingConnection))
|
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription && !l.IsClosingConnection))
|
||||||
{
|
{
|
||||||
subscription.IsClosingConnection = true;
|
subscription.IsClosingConnection = true;
|
||||||
@@ -354,6 +380,10 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
_listeners.Remove(query);
|
_listeners.Remove(query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
_ = Task.Run(() => ConnectionClosed?.Invoke());
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -369,8 +399,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
Authenticated = false;
|
Authenticated = false;
|
||||||
_lastSequenceNumber = 0;
|
_lastSequenceNumber = 0;
|
||||||
|
|
||||||
lock (_listenersLock)
|
try
|
||||||
{
|
{
|
||||||
|
_listenersLock.EnterWriteLock();
|
||||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
|
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
|
||||||
subscription.Reset();
|
subscription.Reset();
|
||||||
|
|
||||||
@@ -380,6 +411,10 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
_listeners.Remove(query);
|
_listeners.Remove(query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
_ = Task.Run(() => ConnectionLost?.Invoke());
|
_ = Task.Run(() => ConnectionLost?.Invoke());
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -401,14 +436,19 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
Status = SocketStatus.Resubscribing;
|
Status = SocketStatus.Resubscribing;
|
||||||
|
|
||||||
lock (_listenersLock)
|
try
|
||||||
{
|
{
|
||||||
|
_listenersLock.EnterWriteLock();
|
||||||
foreach (var query in _listeners.OfType<Query>().ToList())
|
foreach (var query in _listeners.OfType<Query>().ToList())
|
||||||
{
|
{
|
||||||
query.Fail(new WebError("Connection interrupted"));
|
query.Fail(new WebError("Connection interrupted"));
|
||||||
_listeners.Remove(query);
|
_listeners.Remove(query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
// Can't wait for this as it would cause a deadlock
|
// Can't wait for this as it would cause a deadlock
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
@@ -464,10 +504,15 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
protected virtual Task HandleRequestRateLimitedAsync(int requestId)
|
protected virtual Task HandleRequestRateLimitedAsync(int requestId)
|
||||||
{
|
{
|
||||||
Query? query;
|
Query? query;
|
||||||
lock (_listenersLock)
|
try
|
||||||
{
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
if (query == null)
|
if (query == null)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -493,10 +538,15 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
protected virtual Task HandleRequestSentAsync(int requestId)
|
protected virtual Task HandleRequestSentAsync(int requestId)
|
||||||
{
|
{
|
||||||
Query? query;
|
Query? query;
|
||||||
lock (_listenersLock)
|
try
|
||||||
{
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
query = _listeners.OfType<Query>().FirstOrDefault(x => x.Id == requestId);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
if (query == null)
|
if (query == null)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -543,8 +593,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
Type? deserializationType = null;
|
Type? deserializationType = null;
|
||||||
lock (_listenersLock)
|
try
|
||||||
{
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
foreach (var subscription in _listeners)
|
foreach (var subscription in _listeners)
|
||||||
{
|
{
|
||||||
foreach (var route in subscription.MessageRouter.Routes)
|
foreach (var route in subscription.MessageRouter.Routes)
|
||||||
@@ -560,6 +611,10 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
if (deserializationType == null)
|
if (deserializationType == null)
|
||||||
{
|
{
|
||||||
@@ -605,8 +660,9 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
var topicFilter = messageConverter.GetTopicFilter(result);
|
var topicFilter = messageConverter.GetTopicFilter(result);
|
||||||
|
|
||||||
bool processed = false;
|
bool processed = false;
|
||||||
lock (_listenersLock)
|
try
|
||||||
{
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
var currentCount = _listeners.Count;
|
var currentCount = _listeners.Count;
|
||||||
for(var i = 0; i < _listeners.Count; i++)
|
for(var i = 0; i < _listeners.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -633,35 +689,60 @@ 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)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
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]")))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,14 +787,19 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (ApiClient._socketConnections.ContainsKey(SocketId))
|
if (ApiClient._socketConnections.ContainsKey(SocketId))
|
||||||
ApiClient._socketConnections.TryRemove(SocketId, out _);
|
ApiClient._socketConnections.TryRemove(SocketId, out _);
|
||||||
|
|
||||||
lock (_listenersLock)
|
try
|
||||||
{
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
foreach (var subscription in _listeners.OfType<Subscription>())
|
foreach (var subscription in _listeners.OfType<Subscription>())
|
||||||
{
|
{
|
||||||
if (subscription.CancellationTokenRegistration.HasValue)
|
if (subscription.CancellationTokenRegistration.HasValue)
|
||||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
await _socket.CloseAsync().ConfigureAwait(false);
|
await _socket.CloseAsync().ConfigureAwait(false);
|
||||||
_socket.Dispose();
|
_socket.Dispose();
|
||||||
@@ -743,18 +829,30 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
subscription.CancellationTokenRegistration.Value.Dispose();
|
subscription.CancellationTokenRegistration.Value.Dispose();
|
||||||
|
|
||||||
bool anyDuplicateSubscription;
|
bool anyDuplicateSubscription;
|
||||||
lock (_listenersLock)
|
|
||||||
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageRouter.Routes.All(l => subscription.MessageRouter.ContainsCheck(l)));
|
|
||||||
|
|
||||||
bool shouldCloseConnection;
|
bool shouldCloseConnection;
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
|
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.MessageRouter.Routes.All(l => subscription.MessageRouter.ContainsCheck(l)));
|
||||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
|
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Status == SubscriptionStatus.Closing || r.Status == SubscriptionStatus.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
if (!anyDuplicateSubscription)
|
if (!anyDuplicateSubscription)
|
||||||
{
|
{
|
||||||
bool needUnsub;
|
bool needUnsub;
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection;
|
needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
if (needUnsub && _socket.IsOpen)
|
if (needUnsub && _socket.IsOpen)
|
||||||
await UnsubscribeAsync(subscription).ConfigureAwait(false);
|
await UnsubscribeAsync(subscription).ConfigureAwait(false);
|
||||||
@@ -779,8 +877,15 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
await CloseAsync().ConfigureAwait(false);
|
await CloseAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterWriteLock();
|
||||||
_listeners.Remove(subscription);
|
_listeners.Remove(subscription);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
subscription.Status = SubscriptionStatus.Closed;
|
subscription.Status = SubscriptionStatus.Closed;
|
||||||
}
|
}
|
||||||
@@ -803,9 +908,15 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
{
|
{
|
||||||
if (Status != SocketStatus.None && Status != SocketStatus.Connected)
|
if (Status != SocketStatus.None && Status != SocketStatus.Connected)
|
||||||
return false;
|
return false;
|
||||||
|
try
|
||||||
lock (_listenersLock)
|
{
|
||||||
|
_listenersLock.EnterWriteLock();
|
||||||
_listeners.Add(subscription);
|
_listeners.Add(subscription);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
if (subscription.UserSubscription)
|
if (subscription.UserSubscription)
|
||||||
_logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount);
|
_logger.AddingNewSubscription(SocketId, subscription.Id, UserSubscriptionCount);
|
||||||
@@ -818,8 +929,15 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
/// <param name="id"></param>
|
/// <param name="id"></param>
|
||||||
public Subscription? GetSubscription(int id)
|
public Subscription? GetSubscription(int id)
|
||||||
{
|
{
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
return _listeners.OfType<Subscription>().SingleOrDefault(s => s.Id == id);
|
return _listeners.OfType<Subscription>().SingleOrDefault(s => s.Id == id);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -867,15 +985,29 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
|
|
||||||
private async Task SendAndWaitIntAsync(Query query, CancellationToken ct = default)
|
private async Task SendAndWaitIntAsync(Query query, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterWriteLock();
|
||||||
_listeners.Add(query);
|
_listeners.Add(query);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
var sendResult = await SendAsync(query.Id, query.Request, query.Weight).ConfigureAwait(false);
|
var sendResult = await SendAsync(query.Id, query.Request, query.Weight).ConfigureAwait(false);
|
||||||
if (!sendResult)
|
if (!sendResult)
|
||||||
{
|
{
|
||||||
query.Fail(sendResult.Error!);
|
query.Fail(sendResult.Error!);
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterWriteLock();
|
||||||
_listeners.Remove(query);
|
_listeners.Remove(query);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitWriteLock();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,8 +1038,15 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterWriteLock();
|
||||||
_listeners.Remove(query);
|
_listeners.Remove(query);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitWriteLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -930,7 +1069,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");
|
||||||
@@ -1014,8 +1153,16 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
if (!DedicatedRequestConnection.IsDedicatedRequestConnection)
|
if (!DedicatedRequestConnection.IsDedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
bool anySubscriptions;
|
bool anySubscriptions;
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
if (!anySubscriptions)
|
if (!anySubscriptions)
|
||||||
{
|
{
|
||||||
// No need to resubscribe anything
|
// No need to resubscribe anything
|
||||||
@@ -1026,11 +1173,16 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool anyAuthenticated;
|
bool anyAuthenticated;
|
||||||
lock (_listenersLock)
|
try
|
||||||
{
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
|
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
|
||||||
|| DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated;
|
|| DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
if (anyAuthenticated)
|
if (anyAuthenticated)
|
||||||
{
|
{
|
||||||
@@ -1055,8 +1207,15 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
return new CallResult(new WebError("Socket not connected"));
|
return new CallResult(new WebError("Socket not connected"));
|
||||||
|
|
||||||
List<Subscription> subList;
|
List<Subscription> subList;
|
||||||
lock (_listenersLock)
|
try
|
||||||
|
{
|
||||||
|
_listenersLock.EnterReadLock();
|
||||||
subList = _listeners.OfType<Subscription>().Where(x => x.Active).Skip(batch * batchSize).Take(batchSize).ToList();
|
subList = _listeners.OfType<Subscription>().Where(x => x.Active).Skip(batch * batchSize).Take(batchSize).ToList();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_listenersLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
if (subList.Count == 0)
|
if (subList.Count == 0)
|
||||||
break;
|
break;
|
||||||
@@ -1064,40 +1223,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 +1241,71 @@ namespace CryptoExchange.Net.Sockets.Default
|
|||||||
return CallResult.SuccessResult;
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to subscribe a new subscription by sending the subscribe query and wait for the result as needed
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscription">The subscription</param>
|
||||||
|
/// <param name="newSubscription">Whether this is a new subscription, or an existing subscription (resubscribing on reconnected socket)</param>
|
||||||
|
/// <param name="subCancelToken">Cancellation token</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subCompleteHandler = () =>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
subQuery.OnComplete = subCompleteHandler;
|
||||||
|
|
||||||
|
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!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subQuery.ExpectsResponse)
|
||||||
|
subCompleteHandler();
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,7 +332,8 @@ namespace CryptoExchange.Net.Trackers.Klines
|
|||||||
_data.Add(item.OpenTime, item);
|
_data.Add(item.OpenTime, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
_firstTimestamp = _data.Min(v => v.Key);
|
_firstTimestamp = _data.Count == 0 ? null : _data.Min(v => v.Key);
|
||||||
|
|
||||||
ApplyWindow(false);
|
ApplyWindow(false);
|
||||||
_logger.KlineTrackerInitialDataSet(SymbolName, _data.Last().Key);
|
_logger.KlineTrackerInitialDataSet(SymbolName, _data.Last().Key);
|
||||||
}
|
}
|
||||||
@@ -375,7 +376,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_firstTimestamp = _data.Min(x => x.Key);
|
_firstTimestamp = _data.Count == 0 ? null : _data.Min(x => x.Key);
|
||||||
_changed = true;
|
_changed = true;
|
||||||
|
|
||||||
SetSyncStatus();
|
SetSyncStatus();
|
||||||
|
|||||||
@@ -259,11 +259,11 @@ namespace CryptoExchange.Net.Trackers.Trades
|
|||||||
var startTime = Period == null ? DateTime.UtcNow.AddMinutes(-5) : DateTime.UtcNow.Add(-Period.Value);
|
var startTime = Period == null ? DateTime.UtcNow.AddMinutes(-5) : DateTime.UtcNow.Add(-Period.Value);
|
||||||
var request = new GetTradeHistoryRequest(Symbol, startTime, DateTime.UtcNow);
|
var request = new GetTradeHistoryRequest(Symbol, startTime, DateTime.UtcNow);
|
||||||
var data = new List<SharedTrade>();
|
var data = new List<SharedTrade>();
|
||||||
await foreach(var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false))
|
await foreach (var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!result)
|
if (!result)
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
if (Limit != null && data.Count > Limit)
|
if (Limit != null && data.Count > Limit)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,6 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
bool Connected { get; }
|
bool Connected { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Currently tracked symbols. Data for these symbols will be requested when polling.
|
|
||||||
/// Websocket updates will be available for all symbols regardless.
|
|
||||||
/// When new data is received for a symbol which is not yet being tracked it will be added to this list and polled in the future unless the `OnlyTrackProvidedSymbols` option is set in the configuration.
|
|
||||||
/// </summary>
|
|
||||||
IEnumerable<SharedSymbol> TrackedSymbols { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// On connection status change. Might trigger multiple times with the same status depending on the underlying subscriptions.
|
/// On connection status change. Might trigger multiple times with the same status depending on the underlying subscriptions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
||||||
@@ -26,6 +27,13 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Exchange { get; }
|
public string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Currently tracked symbols. Data for these symbols will be requested when polling.
|
||||||
|
/// Websocket updates will be available for all symbols regardless.
|
||||||
|
/// When new data is received for a symbol which is not yet being tracked it will be added to this list and polled in the future unless the `OnlyTrackProvidedSymbols` option is set in the configuration.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<SharedSymbol> TrackedSymbols { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Balances tracker
|
/// Balances tracker
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -57,5 +65,18 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task StopAsync();
|
Task StopAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add symbols to the list of symbols for which data is being tracked
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbols">Symbols to add</param>
|
||||||
|
void AddTrackedSymbolsAsync(IEnumerable<SharedSymbol> symbols);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a symbol from the list of symbols for which data is being tracked.
|
||||||
|
/// Note that the symbol will be added again if new data for that symbol is received, unless the OnlyTrackProvidedSymbols option has been set to true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">Symbol to remove</param>
|
||||||
|
void RemoveTrackedSymbolAsync(SharedSymbol symbol);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
||||||
@@ -26,6 +27,13 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Exchange { get; }
|
public string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Currently tracked symbols. Data for these symbols will be requested when polling.
|
||||||
|
/// Websocket updates will be available for all symbols regardless.
|
||||||
|
/// When new data is received for a symbol which is not yet being tracked it will be added to this list and polled in the future unless the `OnlyTrackProvidedSymbols` option is set in the configuration.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<SharedSymbol> TrackedSymbols { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Balances tracker
|
/// Balances tracker
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -53,5 +61,18 @@ namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task StopAsync();
|
Task StopAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add symbols to the list of symbols for which data is being tracked
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbols">Symbols to add</param>
|
||||||
|
void AddTrackedSymbolsAsync(IEnumerable<SharedSymbol> symbols);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a symbol from the list of symbols for which data is being tracked.
|
||||||
|
/// Note that the symbol will be added again if new data for that symbol is received, unless the OnlyTrackProvidedSymbols option has been set to true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">Symbol to remove</param>
|
||||||
|
void RemoveTrackedSymbolAsync(SharedSymbol symbol);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,12 +22,13 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public BalanceTracker(
|
public BalanceTracker(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
IBalanceRestClient restClient,
|
IBalanceRestClient restClient,
|
||||||
IBalanceSocketClient? socketClient,
|
IBalanceSocketClient? socketClient,
|
||||||
SharedAccountType accountType,
|
SharedAccountType accountType,
|
||||||
TrackerItemConfig config,
|
TrackerItemConfig config,
|
||||||
ExchangeParameters? exchangeParameters = null
|
ExchangeParameters? exchangeParameters = null
|
||||||
) : base(logger, UserDataType.Balances, restClient.Exchange, config, false, null)
|
) : base(logger, symbolTracker, UserDataType.Balances, restClient.Exchange, config)
|
||||||
{
|
{
|
||||||
if (_socketClient == null)
|
if (_socketClient == null)
|
||||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -27,13 +28,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public FuturesOrderTracker(
|
public FuturesOrderTracker(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
IFuturesOrderRestClient restClient,
|
IFuturesOrderRestClient restClient,
|
||||||
IFuturesOrderSocketClient? socketClient,
|
IFuturesOrderSocketClient? socketClient,
|
||||||
TrackerItemConfig config,
|
TrackerItemConfig config,
|
||||||
IEnumerable<SharedSymbol> symbols,
|
IEnumerable<SharedSymbol> symbols,
|
||||||
bool onlyTrackProvidedSymbols,
|
bool onlyTrackProvidedSymbols,
|
||||||
ExchangeParameters? exchangeParameters = null
|
ExchangeParameters? exchangeParameters = null
|
||||||
) : base(logger, UserDataType.Orders, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
) : base(logger, symbolTracker, UserDataType.Orders, restClient.Exchange, config)
|
||||||
{
|
{
|
||||||
if (_socketClient == null)
|
if (_socketClient == null)
|
||||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
@@ -45,6 +47,20 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
_requiresSymbolParameterOpenOrders = restClient.GetOpenFuturesOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol");
|
_requiresSymbolParameterOpenOrders = restClient.GetOpenFuturesOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void ClearDataForSymbol(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
foreach (var order in _store)
|
||||||
|
{
|
||||||
|
if (order.Value.SharedSymbol!.TradingMode == symbol.TradingMode
|
||||||
|
&& order.Value.SharedSymbol.BaseAsset == symbol.BaseAsset
|
||||||
|
&& order.Value.SharedSymbol.QuoteAsset == symbol.QuoteAsset
|
||||||
|
&& order.Value.SharedSymbol.DeliverTime == symbol.DeliverTime)
|
||||||
|
{
|
||||||
|
_store.TryRemove(order.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override bool Update(SharedFuturesOrder existingItem, SharedFuturesOrder updateItem)
|
protected override bool Update(SharedFuturesOrder existingItem, SharedFuturesOrder updateItem)
|
||||||
{
|
{
|
||||||
@@ -129,6 +145,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;
|
||||||
@@ -225,7 +249,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var symbol in _symbols.ToList())
|
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||||
{
|
{
|
||||||
var openOrdersResult = await _restClient.GetOpenFuturesOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var openOrdersResult = await _restClient.GetOpenFuturesOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!openOrdersResult.Success)
|
if (!openOrdersResult.Success)
|
||||||
@@ -243,11 +267,30 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var symbol in _symbols.ToList())
|
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))
|
||||||
{
|
{
|
||||||
var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
if (openOrders.Any(x => x.OrderId == order.OrderId))
|
||||||
var updatedPollTime = DateTime.UtcNow;
|
continue;
|
||||||
|
|
||||||
|
if (!_openOrderNotReturnedTimes.ContainsKey(order.OrderId))
|
||||||
|
_openOrderNotReturnedTimes[order.OrderId] = 0;
|
||||||
|
|
||||||
|
_openOrderNotReturnedTimes[order.OrderId] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
|
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||||
|
{
|
||||||
|
DateTime? fromTimeOrders = GetClosedOrdersRequestStartTime(symbol);
|
||||||
|
|
||||||
var 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)
|
||||||
{
|
{
|
||||||
@@ -259,22 +302,26 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_lastDataTimeBeforeDisconnect = null;
|
|
||||||
_lastPollTime = updatedPollTime;
|
|
||||||
|
|
||||||
// Filter orders to only include where close time is after the start time
|
// Filter orders to only include where close time is after the start time
|
||||||
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
||||||
x.UpdateTime != null && x.UpdateTime >= _startTime // Updated after the tracker start time
|
(x.UpdateTime != null && x.UpdateTime >= _startTime) // Updated after the tracker start time
|
||||||
|| x.CreateTime != null && x.CreateTime >= _startTime // Created after the tracker start time
|
|| (x.CreateTime != null && x.CreateTime >= _startTime) // Created after the tracker start time
|
||||||
|| x.CreateTime == null && x.UpdateTime == null // Unknown time
|
|| (x.CreateTime == null && x.UpdateTime == null) // Unknown time
|
||||||
|
|| (Values.Any(e => e.OrderId == x.OrderId && x.Status == SharedOrderStatus.Open)) // Or we're currently tracking this open order
|
||||||
).ToArray();
|
).ToArray();
|
||||||
|
|
||||||
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
||||||
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>();
|
||||||
@@ -292,7 +339,64 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!anyError)
|
||||||
|
{
|
||||||
|
_lastPollTime = updatedPollTime;
|
||||||
|
_lastDataTimeBeforeDisconnect = null;
|
||||||
|
}
|
||||||
|
|
||||||
return anyError;
|
return anyError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DateTime? GetClosedOrdersRequestStartTime(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
// Determine the timestamp from which we need to check order status
|
||||||
|
// Use the timestamp we last know the correct state of the data
|
||||||
|
DateTime? fromTime = null;
|
||||||
|
string? source = null;
|
||||||
|
|
||||||
|
// Use the last timestamp we we received data from the websocket as state should be correct at that time. 1 seconds buffer
|
||||||
|
if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value))
|
||||||
|
{
|
||||||
|
fromTime = _lastDataTimeBeforeDisconnect.Value.AddSeconds(-1);
|
||||||
|
source = "LastDataTimeBeforeDisconnect";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've previously polled use that timestamp to request data from
|
||||||
|
if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime))
|
||||||
|
{
|
||||||
|
fromTime = _lastPollTime;
|
||||||
|
source = "LastPollTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we known open orders with a create time before this time we need to use that timestamp to make sure that order is included in the response
|
||||||
|
var trackedOrdersMinOpenTime = Values
|
||||||
|
.Where(x => x.Status == SharedOrderStatus.Open && x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset)
|
||||||
|
.OrderBy(x => x.CreateTime)
|
||||||
|
.FirstOrDefault()?.CreateTime;
|
||||||
|
if (trackedOrdersMinOpenTime.HasValue && (fromTime == null || trackedOrdersMinOpenTime.Value < fromTime))
|
||||||
|
{
|
||||||
|
// Could be improved by only requesting the specific open orders if there are only a few that would be better than trying to request a long
|
||||||
|
// history if the open order is far back
|
||||||
|
fromTime = trackedOrdersMinOpenTime.Value.AddMilliseconds(-1);
|
||||||
|
source = "OpenOrder";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromTime == null)
|
||||||
|
{
|
||||||
|
fromTime = _startTime;
|
||||||
|
source = "StartTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(1))
|
||||||
|
{
|
||||||
|
// Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync
|
||||||
|
fromTime = DateTime.UtcNow.AddSeconds(-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogTrace("{DataType}.{Symbol} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}",
|
||||||
|
DataType, $"{symbol.BaseAsset}/{symbol.QuoteAsset}", source, fromTime);
|
||||||
|
return fromTime!.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public FuturesUserTradeTracker(
|
public FuturesUserTradeTracker(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
IFuturesOrderRestClient restClient,
|
IFuturesOrderRestClient restClient,
|
||||||
IUserTradeSocketClient? socketClient,
|
IUserTradeSocketClient? socketClient,
|
||||||
TrackerItemConfig config,
|
TrackerItemConfig config,
|
||||||
IEnumerable<SharedSymbol> symbols,
|
IEnumerable<SharedSymbol> symbols,
|
||||||
bool onlyTrackProvidedSymbols,
|
bool onlyTrackProvidedSymbols,
|
||||||
ExchangeParameters? exchangeParameters = null
|
ExchangeParameters? exchangeParameters = null
|
||||||
) : base(logger, UserDataType.Trades, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
) : base(logger, symbolTracker, UserDataType.Trades, restClient.Exchange, config)
|
||||||
{
|
{
|
||||||
if (_socketClient == null)
|
if (_socketClient == null)
|
||||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
@@ -42,6 +43,20 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
_exchangeParameters = exchangeParameters;
|
_exchangeParameters = exchangeParameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void ClearDataForSymbol(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
foreach (var order in _store)
|
||||||
|
{
|
||||||
|
if (order.Value.SharedSymbol!.TradingMode == symbol.TradingMode
|
||||||
|
&& order.Value.SharedSymbol.BaseAsset == symbol.BaseAsset
|
||||||
|
&& order.Value.SharedSymbol.QuoteAsset == symbol.QuoteAsset
|
||||||
|
&& order.Value.SharedSymbol.DeliverTime == symbol.DeliverTime)
|
||||||
|
{
|
||||||
|
_store.TryRemove(order.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override string GetKey(SharedUserTrade item) => item.Id;
|
protected override string GetKey(SharedUserTrade item) => item.Id;
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -55,10 +70,10 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
protected override async Task<bool> DoPollAsync()
|
protected override async Task<bool> DoPollAsync()
|
||||||
{
|
{
|
||||||
var anyError = false;
|
var anyError = false;
|
||||||
foreach (var symbol in _symbols)
|
var fromTimeTrades = GetTradesRequestStartTime();
|
||||||
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
|
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||||
{
|
{
|
||||||
var fromTimeTrades = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
|
||||||
var updatedPollTime = DateTime.UtcNow;
|
|
||||||
var tradesResult = await _restClient.GetFuturesUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var tradesResult = await _restClient.GetFuturesUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!tradesResult.Success)
|
if (!tradesResult.Success)
|
||||||
{
|
{
|
||||||
@@ -80,9 +95,53 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!anyError)
|
||||||
|
{
|
||||||
|
_lastDataTimeBeforeDisconnect = null;
|
||||||
|
_lastPollTime = updatedPollTime;
|
||||||
|
}
|
||||||
|
|
||||||
return anyError;
|
return anyError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DateTime? GetTradesRequestStartTime()
|
||||||
|
{
|
||||||
|
// Determine the timestamp from which we need to check order status
|
||||||
|
// Use the timestamp we last know the correct state of the data
|
||||||
|
DateTime? fromTime = null;
|
||||||
|
string? source = null;
|
||||||
|
|
||||||
|
// Use the last timestamp we we received data from the websocket as state should be correct at that time. 1 seconds buffer
|
||||||
|
if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value))
|
||||||
|
{
|
||||||
|
fromTime = _lastDataTimeBeforeDisconnect.Value.AddSeconds(-1);
|
||||||
|
source = "LastDataTimeBeforeDisconnect";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've previously polled use that timestamp to request data from
|
||||||
|
if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime))
|
||||||
|
{
|
||||||
|
fromTime = _lastPollTime;
|
||||||
|
source = "LastPollTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromTime == null)
|
||||||
|
{
|
||||||
|
fromTime = _startTime;
|
||||||
|
source = "StartTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (now - fromTime < TimeSpan.FromSeconds(1))
|
||||||
|
{
|
||||||
|
// Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync
|
||||||
|
fromTime = DateTime.UtcNow.AddSeconds(-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogTrace("{DataType} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}", DataType, source, fromTime);
|
||||||
|
return fromTime!.Value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public PositionTracker(
|
public PositionTracker(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
IFuturesOrderRestClient restClient,
|
IFuturesOrderRestClient restClient,
|
||||||
IPositionSocketClient? socketClient,
|
IPositionSocketClient? socketClient,
|
||||||
TrackerItemConfig config,
|
TrackerItemConfig config,
|
||||||
@@ -36,7 +37,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
bool onlyTrackProvidedSymbols,
|
bool onlyTrackProvidedSymbols,
|
||||||
bool websocketPositionUpdatesAreFullSnapshots,
|
bool websocketPositionUpdatesAreFullSnapshots,
|
||||||
ExchangeParameters? exchangeParameters = null
|
ExchangeParameters? exchangeParameters = null
|
||||||
) : base(logger, UserDataType.Positions, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
) : base(logger, symbolTracker, UserDataType.Positions, restClient.Exchange, config)
|
||||||
{
|
{
|
||||||
if (_socketClient == null)
|
if (_socketClient == null)
|
||||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
@@ -118,9 +119,9 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
{
|
{
|
||||||
toRemove ??= new List<SharedPosition>();
|
toRemove ??= new List<SharedPosition>();
|
||||||
toRemove.Add(item);
|
toRemove.Add(item);
|
||||||
|
_logger.LogTrace("Ignoring {DataType} update for {Key}, no SharedSymbol set", DataType, item.Symbol);
|
||||||
}
|
}
|
||||||
else if (_onlyTrackProvidedSymbols
|
else if (!_symbolTracker.ShouldProcess(symbolModel.SharedSymbol))
|
||||||
&& !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
|
|
||||||
{
|
{
|
||||||
toRemove ??= new List<SharedPosition>();
|
toRemove ??= new List<SharedPosition>();
|
||||||
toRemove.Add(item);
|
toRemove.Add(item);
|
||||||
@@ -131,8 +132,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
if (toRemove != null)
|
if (toRemove != null)
|
||||||
@event = @event.Except(toRemove).ToArray();
|
@event = @event.Except(toRemove).ToArray();
|
||||||
|
|
||||||
if (!_onlyTrackProvidedSymbols)
|
_symbolTracker.UpdateTrackedSymbols(@event.Where(x => x.PositionSize > 0).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;
|
||||||
|
|
||||||
@@ -27,13 +28,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public SpotOrderTracker(
|
public SpotOrderTracker(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
ISpotOrderRestClient restClient,
|
ISpotOrderRestClient restClient,
|
||||||
ISpotOrderSocketClient? socketClient,
|
ISpotOrderSocketClient? socketClient,
|
||||||
TrackerItemConfig config,
|
TrackerItemConfig config,
|
||||||
IEnumerable<SharedSymbol> symbols,
|
IEnumerable<SharedSymbol> symbols,
|
||||||
bool onlyTrackProvidedSymbols,
|
bool onlyTrackProvidedSymbols,
|
||||||
ExchangeParameters? exchangeParameters = null
|
ExchangeParameters? exchangeParameters = null
|
||||||
) : base(logger, UserDataType.Orders, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
) : base(logger, symbolTracker, UserDataType.Orders, restClient.Exchange, config)
|
||||||
{
|
{
|
||||||
if (_socketClient == null)
|
if (_socketClient == null)
|
||||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
@@ -45,6 +47,19 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
_requiresSymbolParameterOpenOrders = restClient.GetOpenSpotOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol");
|
_requiresSymbolParameterOpenOrders = restClient.GetOpenSpotOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void ClearDataForSymbol(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
foreach(var order in _store)
|
||||||
|
{
|
||||||
|
if (order.Value.SharedSymbol!.TradingMode == symbol.TradingMode
|
||||||
|
&& order.Value.SharedSymbol.BaseAsset == symbol.BaseAsset
|
||||||
|
&& order.Value.SharedSymbol.QuoteAsset == symbol.QuoteAsset)
|
||||||
|
{
|
||||||
|
_store.TryRemove(order.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override bool Update(SharedSpotOrder existingItem, SharedSpotOrder updateItem)
|
protected override bool Update(SharedSpotOrder existingItem, SharedSpotOrder updateItem)
|
||||||
{
|
{
|
||||||
@@ -140,6 +155,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;
|
||||||
@@ -236,7 +259,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var symbol in _symbols.ToList())
|
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||||
{
|
{
|
||||||
var openOrdersResult = await _restClient.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var openOrdersResult = await _restClient.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!openOrdersResult.Success)
|
if (!openOrdersResult.Success)
|
||||||
@@ -258,10 +281,27 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
if (!_firstPollDone && anyError)
|
if (!_firstPollDone && anyError)
|
||||||
return anyError;
|
return anyError;
|
||||||
|
|
||||||
foreach (var symbol in _symbols.ToList())
|
// 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))
|
||||||
{
|
{
|
||||||
var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
if (openOrders.Any(x => x.OrderId == order.OrderId))
|
||||||
var updatedPollTime = DateTime.UtcNow;
|
continue;
|
||||||
|
|
||||||
|
if (!_openOrderNotReturnedTimes.ContainsKey(order.OrderId))
|
||||||
|
_openOrderNotReturnedTimes[order.OrderId] = 0;
|
||||||
|
|
||||||
|
_openOrderNotReturnedTimes[order.OrderId] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
|
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||||
|
{
|
||||||
|
DateTime? fromTimeOrders = GetClosedOrdersRequestStartTime(symbol);
|
||||||
|
|
||||||
|
|
||||||
var 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)
|
||||||
{
|
{
|
||||||
@@ -273,22 +313,26 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_lastDataTimeBeforeDisconnect = null;
|
|
||||||
_lastPollTime = updatedPollTime;
|
|
||||||
|
|
||||||
// Filter orders to only include where close time is after the start time
|
// Filter orders to only include where close time is after the start time
|
||||||
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
||||||
x.UpdateTime != null && x.UpdateTime >= _startTime // Updated after the tracker start time
|
(x.UpdateTime != null && x.UpdateTime >= _startTime) // Updated after the tracker start time
|
||||||
|| x.CreateTime != null && x.CreateTime >= _startTime // Created after the tracker start time
|
|| (x.CreateTime != null && x.CreateTime >= _startTime) // Created after the tracker start time
|
||||||
|| x.CreateTime == null && x.UpdateTime == null // Unknown time
|
|| (x.CreateTime == null && x.UpdateTime == null) // Unknown time
|
||||||
|
|| (Values.Any(e => e.OrderId == x.OrderId && x.Status == SharedOrderStatus.Open)) // Or we're currently tracking this open order
|
||||||
).ToArray();
|
).ToArray();
|
||||||
|
|
||||||
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
||||||
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>();
|
||||||
@@ -306,7 +350,64 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!anyError)
|
||||||
|
{
|
||||||
|
_lastDataTimeBeforeDisconnect = null;
|
||||||
|
_lastPollTime = updatedPollTime;
|
||||||
|
}
|
||||||
|
|
||||||
return anyError;
|
return anyError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DateTime? GetClosedOrdersRequestStartTime(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
// Determine the timestamp from which we need to check order status
|
||||||
|
// Use the timestamp we last know the correct state of the data
|
||||||
|
DateTime? fromTime = null;
|
||||||
|
string? source = null;
|
||||||
|
|
||||||
|
// Use the last timestamp we we received data from the websocket as state should be correct at that time. 1 seconds buffer
|
||||||
|
if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value))
|
||||||
|
{
|
||||||
|
fromTime = _lastDataTimeBeforeDisconnect.Value.AddSeconds(-1);
|
||||||
|
source = "LastDataTimeBeforeDisconnect";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've previously polled use that timestamp to request data from
|
||||||
|
if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime))
|
||||||
|
{
|
||||||
|
fromTime = _lastPollTime;
|
||||||
|
source = "LastPollTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we known open orders with a create time before this time we need to use that timestamp to make sure that order is included in the response
|
||||||
|
var trackedOrdersMinOpenTime = Values
|
||||||
|
.Where(x => x.Status == SharedOrderStatus.Open && x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset)
|
||||||
|
.OrderBy(x => x.CreateTime)
|
||||||
|
.FirstOrDefault()?.CreateTime;
|
||||||
|
if (trackedOrdersMinOpenTime.HasValue && (fromTime == null || trackedOrdersMinOpenTime.Value < fromTime))
|
||||||
|
{
|
||||||
|
// Could be improved by only requesting the specific open orders if there are only a few that would be better than trying to request a long
|
||||||
|
// history if the open order is far back
|
||||||
|
fromTime = trackedOrdersMinOpenTime.Value.AddMilliseconds(-1);
|
||||||
|
source = "OpenOrder";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromTime == null)
|
||||||
|
{
|
||||||
|
fromTime = _startTime;
|
||||||
|
source = "StartTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(1))
|
||||||
|
{
|
||||||
|
// Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync
|
||||||
|
fromTime = DateTime.UtcNow.AddSeconds(-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogTrace("{DataType}.{Symbol} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}",
|
||||||
|
DataType, $"{symbol.BaseAsset}/{symbol.QuoteAsset}", source, fromTime);
|
||||||
|
return fromTime!.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public SpotUserTradeTracker(
|
public SpotUserTradeTracker(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
ISpotOrderRestClient restClient,
|
ISpotOrderRestClient restClient,
|
||||||
IUserTradeSocketClient? socketClient,
|
IUserTradeSocketClient? socketClient,
|
||||||
TrackerItemConfig config,
|
TrackerItemConfig config,
|
||||||
IEnumerable<SharedSymbol> symbols,
|
IEnumerable<SharedSymbol> symbols,
|
||||||
bool onlyTrackProvidedSymbols,
|
bool onlyTrackProvidedSymbols,
|
||||||
ExchangeParameters? exchangeParameters = null
|
ExchangeParameters? exchangeParameters = null
|
||||||
) : base(logger, UserDataType.Trades, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
) : base(logger, symbolTracker, UserDataType.Trades, restClient.Exchange, config)
|
||||||
{
|
{
|
||||||
if (_socketClient == null)
|
if (_socketClient == null)
|
||||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||||
@@ -42,6 +43,19 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
_exchangeParameters = exchangeParameters;
|
_exchangeParameters = exchangeParameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void ClearDataForSymbol(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
foreach (var trade in _store)
|
||||||
|
{
|
||||||
|
if (trade.Value.SharedSymbol!.TradingMode == symbol.TradingMode
|
||||||
|
&& trade.Value.SharedSymbol.BaseAsset == symbol.BaseAsset
|
||||||
|
&& trade.Value.SharedSymbol.QuoteAsset == symbol.QuoteAsset)
|
||||||
|
{
|
||||||
|
_store.TryRemove(trade.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override string GetKey(SharedUserTrade item) => item.Id;
|
protected override string GetKey(SharedUserTrade item) => item.Id;
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -55,10 +69,10 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
protected override async Task<bool> DoPollAsync()
|
protected override async Task<bool> DoPollAsync()
|
||||||
{
|
{
|
||||||
var anyError = false;
|
var anyError = false;
|
||||||
foreach (var symbol in _symbols)
|
var fromTimeTrades = GetTradesRequestStartTime();
|
||||||
|
var updatedPollTime = DateTime.UtcNow;
|
||||||
|
foreach (var symbol in _symbolTracker.GetTrackedSymbols())
|
||||||
{
|
{
|
||||||
var fromTimeTrades = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
|
||||||
var updatedPollTime = DateTime.UtcNow;
|
|
||||||
var tradesResult = await _restClient.GetSpotUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
var tradesResult = await _restClient.GetSpotUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||||
if (!tradesResult.Success)
|
if (!tradesResult.Success)
|
||||||
{
|
{
|
||||||
@@ -70,8 +84,6 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_lastDataTimeBeforeDisconnect = null;
|
|
||||||
_lastPollTime = updatedPollTime;
|
|
||||||
|
|
||||||
// Filter trades to only include where timestamp is after the start time OR it's part of an order we're tracking
|
// Filter trades to only include where timestamp is after the start time OR it's part of an order we're tracking
|
||||||
var relevantTrades = tradesResult.Data.Where(x => x.Timestamp >= _startTime || (GetTrackedOrderIds?.Invoke() ?? []).Any(o => o == x.OrderId)).ToArray();
|
var relevantTrades = tradesResult.Data.Where(x => x.Timestamp >= _startTime || (GetTrackedOrderIds?.Invoke() ?? []).Any(o => o == x.OrderId)).ToArray();
|
||||||
@@ -80,9 +92,52 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!anyError)
|
||||||
|
{
|
||||||
|
_lastDataTimeBeforeDisconnect = null;
|
||||||
|
_lastPollTime = updatedPollTime;
|
||||||
|
}
|
||||||
|
|
||||||
return anyError;
|
return anyError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DateTime? GetTradesRequestStartTime()
|
||||||
|
{
|
||||||
|
// Determine the timestamp from which we need to check order status
|
||||||
|
// Use the timestamp we last know the correct state of the data
|
||||||
|
DateTime? fromTime = null;
|
||||||
|
string? source = null;
|
||||||
|
|
||||||
|
// Use the last timestamp we we received data from the websocket as state should be correct at that time. 1 seconds buffer
|
||||||
|
if (_lastDataTimeBeforeDisconnect.HasValue && (fromTime == null || fromTime > _lastDataTimeBeforeDisconnect.Value))
|
||||||
|
{
|
||||||
|
fromTime = _lastDataTimeBeforeDisconnect.Value.AddSeconds(-1);
|
||||||
|
source = "LastDataTimeBeforeDisconnect";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've previously polled use that timestamp to request data from
|
||||||
|
if (_lastPollTime.HasValue && (fromTime == null || _lastPollTime.Value > fromTime))
|
||||||
|
{
|
||||||
|
fromTime = _lastPollTime;
|
||||||
|
source = "LastPollTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromTime == null)
|
||||||
|
{
|
||||||
|
fromTime = _startTime;
|
||||||
|
source = "StartTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - fromTime < TimeSpan.FromSeconds(1))
|
||||||
|
{
|
||||||
|
// Set it to at least 5 seconds in the past to prevent issues when local time isn't in sync
|
||||||
|
fromTime = DateTime.UtcNow.AddSeconds(-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogTrace("{DataType} UserDataTracker poll startTime filter based on {Source}: {Time:yyyy-MM-dd HH:mm:ss.fff}", DataType, source, fromTime);
|
||||||
|
return fromTime!.Value;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -203,21 +203,14 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected ConcurrentDictionary<string, T> _store = new ConcurrentDictionary<string, T>(StringComparer.InvariantCultureIgnoreCase);
|
protected ConcurrentDictionary<string, T> _store = new ConcurrentDictionary<string, T>(StringComparer.InvariantCultureIgnoreCase);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tracked symbols list
|
|
||||||
/// </summary>
|
|
||||||
protected readonly List<SharedSymbol> _symbols;
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol lock
|
|
||||||
/// </summary>
|
|
||||||
protected object _symbolLock = new object();
|
|
||||||
/// <summary>
|
|
||||||
/// Only track provided symbols setting
|
|
||||||
/// </summary>
|
|
||||||
protected bool _onlyTrackProvidedSymbols;
|
|
||||||
/// <summary>
|
|
||||||
/// Is SharedSymbol model
|
/// Is SharedSymbol model
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _isSymbolModel;
|
protected bool _isSymbolModel;
|
||||||
|
/// <summary>
|
||||||
|
/// Symbol tracker
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
protected readonly UserDataSymbolTracker _symbolTracker;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public T[] Values
|
public T[] Values
|
||||||
@@ -240,22 +233,23 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Func<UserDataUpdate<T[]>, Task>? OnUpdate;
|
public event Func<UserDataUpdate<T[]>, Task>? OnUpdate;
|
||||||
/// <inheritdoc />
|
|
||||||
public IEnumerable<SharedSymbol> TrackedSymbols => _symbols;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public UserDataItemTracker(ILogger logger, UserDataType dataType, string exchange, TrackerItemConfig config, bool onlyTrackProvidedSymbols, IEnumerable<SharedSymbol>? symbols) : base(logger, dataType, exchange)
|
public UserDataItemTracker(
|
||||||
|
ILogger logger,
|
||||||
|
UserDataSymbolTracker symbolTracker,
|
||||||
|
UserDataType dataType,
|
||||||
|
string exchange,
|
||||||
|
TrackerItemConfig config) : base(logger, dataType, exchange)
|
||||||
{
|
{
|
||||||
_onlyTrackProvidedSymbols = onlyTrackProvidedSymbols;
|
|
||||||
_symbols = symbols?.ToList() ?? [];
|
|
||||||
|
|
||||||
_pollIntervalDisconnected = config.PollIntervalDisconnected;
|
_pollIntervalDisconnected = config.PollIntervalDisconnected;
|
||||||
_pollIntervalConnected = config.PollIntervalConnected;
|
_pollIntervalConnected = config.PollIntervalConnected;
|
||||||
_pollAtStart = config.PollAtStart;
|
_pollAtStart = config.PollAtStart;
|
||||||
_retentionTime = config is TrackerTimedItemConfig timeConfig ? timeConfig.RetentionTime : TimeSpan.MaxValue;
|
_retentionTime = config is TrackerTimedItemConfig timeConfig ? timeConfig.RetentionTime : TimeSpan.MaxValue;
|
||||||
_isSymbolModel = typeof(T).IsSubclassOf(typeof(SharedSymbolModel));
|
_isSymbolModel = typeof(T).IsSubclassOf(typeof(SharedSymbolModel));
|
||||||
|
_symbolTracker = symbolTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -334,26 +328,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
/// Get the age of an item
|
/// Get the age of an item
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual TimeSpan GetAge(DateTime time, T item) => TimeSpan.Zero;
|
protected virtual TimeSpan GetAge(DateTime time, T item) => TimeSpan.Zero;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Update the tracked symbol list with potential new symbols
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="symbols"></param>
|
|
||||||
protected void UpdateSymbolsList(IEnumerable<SharedSymbol> symbols)
|
|
||||||
{
|
|
||||||
lock (_symbolLock)
|
|
||||||
{
|
|
||||||
foreach (var symbol in symbols.Distinct())
|
|
||||||
{
|
|
||||||
if (!_symbols.Any(x => x.TradingMode == symbol.TradingMode && x.BaseAsset == symbol.BaseAsset && x.QuoteAsset == symbol.QuoteAsset))
|
|
||||||
{
|
|
||||||
_symbols.Add(symbol);
|
|
||||||
_logger.LogDebug("Adding {BaseAsset}/{QuoteAsset} to symbol tracking list", symbol.BaseAsset, symbol.QuoteAsset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle an update
|
/// Handle an update
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -372,9 +347,9 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
{
|
{
|
||||||
toRemove ??= new List<T>();
|
toRemove ??= new List<T>();
|
||||||
toRemove.Add(item);
|
toRemove.Add(item);
|
||||||
|
_logger.LogWarning("Ignoring {DataType} update for {Key}, no SharedSymbol set", DataType, GetKey(item));
|
||||||
}
|
}
|
||||||
else if (_onlyTrackProvidedSymbols
|
else if (!_symbolTracker.ShouldProcess(symbolModel.SharedSymbol))
|
||||||
&& !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
|
|
||||||
{
|
{
|
||||||
toRemove ??= new List<T>();
|
toRemove ??= new List<T>();
|
||||||
toRemove.Add(item);
|
toRemove.Add(item);
|
||||||
@@ -385,8 +360,7 @@ namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
|||||||
if (toRemove != null)
|
if (toRemove != null)
|
||||||
@event = @event.Except(toRemove).ToArray();
|
@event = @event.Except(toRemove).ToArray();
|
||||||
|
|
||||||
if (!_onlyTrackProvidedSymbols)
|
_symbolTracker.UpdateTrackedSymbols(@event.OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
||||||
UpdateSymbolsList(@event.OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update local store
|
// Update local store
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Trackers.UserData.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tracker for symbols used in UserDataTracker
|
||||||
|
/// </summary>
|
||||||
|
public class UserDataSymbolTracker
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly List<SharedSymbol> _trackedSymbols;
|
||||||
|
private readonly bool _onlyTrackProvidedSymbols;
|
||||||
|
private readonly object _symbolLock = new object();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public UserDataSymbolTracker(ILogger logger, UserDataTrackerConfig config)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_trackedSymbols = config.TrackedSymbols?.ToList() ?? [];
|
||||||
|
_onlyTrackProvidedSymbols = config.OnlyTrackProvidedSymbols;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get currently tracked symbols
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public IEnumerable<SharedSymbol> GetTrackedSymbols()
|
||||||
|
{
|
||||||
|
lock (_symbolLock)
|
||||||
|
return _trackedSymbols.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether a symbol is in the tracked symbols list and should be processed
|
||||||
|
/// </summary>
|
||||||
|
public bool ShouldProcess(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
if (!_onlyTrackProvidedSymbols)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return _trackedSymbols.Any(y => y.TradingMode == symbol!.TradingMode && y.BaseAsset == symbol.BaseAsset && y.QuoteAsset == symbol.QuoteAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the tracked symbol list with potential new symbols
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateTrackedSymbols(IEnumerable<SharedSymbol> symbols, bool addByUser = false)
|
||||||
|
{
|
||||||
|
if (!addByUser && _onlyTrackProvidedSymbols)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_symbolLock)
|
||||||
|
{
|
||||||
|
foreach (var symbol in symbols.Distinct())
|
||||||
|
{
|
||||||
|
if (!_trackedSymbols.Any(x => x.TradingMode == symbol.TradingMode && x.BaseAsset == symbol.BaseAsset && x.QuoteAsset == symbol.QuoteAsset))
|
||||||
|
{
|
||||||
|
_trackedSymbols.Add(symbol);
|
||||||
|
_logger.LogDebug("Adding {TradingMode}.{BaseAsset}/{QuoteAsset} to symbol tracking list", symbol.TradingMode, symbol.BaseAsset, symbol.QuoteAsset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a symbol from the list
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveTrackedSymbol(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
lock (_symbolLock)
|
||||||
|
{
|
||||||
|
var symbolToRemove = _trackedSymbols.SingleOrDefault(x => x.TradingMode == symbol.TradingMode && x.BaseAsset == symbol.BaseAsset && x.QuoteAsset == symbol.QuoteAsset);
|
||||||
|
if (symbolToRemove != null)
|
||||||
|
_trackedSymbols.Remove(symbolToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
|
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
|
||||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||||
using Microsoft.Extensions.Logging;
|
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,11 +24,21 @@ 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>
|
||||||
protected abstract UserDataItemTracker[] DataTrackers { get; }
|
protected abstract UserDataItemTracker[] DataTrackers { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Symbol tracker
|
||||||
|
/// </summary>
|
||||||
|
protected internal UserDataSymbolTracker SymbolTracker { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string? UserIdentifier { get; }
|
public string? UserIdentifier { get; }
|
||||||
|
|
||||||
@@ -45,6 +57,11 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Connected => DataTrackers.All(x => x.Connected);
|
public bool Connected => DataTrackers.All(x => x.Connected);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Currently tracked symbols
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<SharedSymbol> TrackedSymbols => SymbolTracker.GetTrackedSymbols();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -54,11 +71,9 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
UserDataTrackerConfig config,
|
UserDataTrackerConfig config,
|
||||||
string? userIdentifier)
|
string? userIdentifier)
|
||||||
{
|
{
|
||||||
if (config.OnlyTrackProvidedSymbols && !config.TrackedSymbols.Any())
|
|
||||||
throw new ArgumentException(nameof(config.TrackedSymbols), "Conflicting options; `OnlyTrackProvidedSymbols` but no symbols specific in `TrackedSymbols`");
|
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
|
SymbolTracker = new UserDataSymbolTracker(logger, config);
|
||||||
Exchange = exchange;
|
Exchange = exchange;
|
||||||
UserIdentifier = userIdentifier;
|
UserIdentifier = userIdentifier;
|
||||||
}
|
}
|
||||||
@@ -68,6 +83,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 +117,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,24 +70,28 @@ 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, SymbolTracker, 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);
|
||||||
|
|
||||||
var orderTracker = new FuturesOrderTracker(logger, futuresOrderRestClient, futuresOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
var orderTracker = new FuturesOrderTracker(logger, SymbolTracker, futuresOrderRestClient, futuresOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||||
Orders = orderTracker;
|
Orders = orderTracker;
|
||||||
trackers.Add(orderTracker);
|
trackers.Add(orderTracker);
|
||||||
|
|
||||||
var positionTracker = new PositionTracker(logger, futuresOrderRestClient, positionSocketClient, config.PositionConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, WebsocketPositionUpdatesAreFullSnapshots, exchangeParameters);
|
var positionTracker = new PositionTracker(logger, SymbolTracker, futuresOrderRestClient, positionSocketClient, config.PositionConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, WebsocketPositionUpdatesAreFullSnapshots, exchangeParameters);
|
||||||
Positions = positionTracker;
|
Positions = positionTracker;
|
||||||
trackers.Add(positionTracker);
|
trackers.Add(positionTracker);
|
||||||
|
|
||||||
if (config.TrackTrades)
|
if (config.TrackTrades)
|
||||||
{
|
{
|
||||||
var tradeTracker = new FuturesUserTradeTracker(logger, futuresOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
var tradeTracker = new FuturesUserTradeTracker(logger, SymbolTracker, futuresOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||||
Trades = tradeTracker;
|
Trades = tradeTracker;
|
||||||
trackers.Add(tradeTracker);
|
trackers.Add(tradeTracker);
|
||||||
|
|
||||||
@@ -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,70 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add symbols to the list of symbols for which data is being tracked
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbols">Symbols to add</param>
|
||||||
|
public void AddTrackedSymbolsAsync(IEnumerable<SharedSymbol> symbols)
|
||||||
|
{
|
||||||
|
if (symbols.Any(x => x.TradingMode == TradingMode.Spot))
|
||||||
|
throw new ArgumentException("Spot symbol not allowed in futures tracker", nameof(symbols));
|
||||||
|
|
||||||
|
SymbolTracker.UpdateTrackedSymbols(symbols, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a symbol from the list of symbols for which data is being tracked.
|
||||||
|
/// Note that the symbol will be added again if new data for that symbol is received, unless the OnlyTrackProvidedSymbols option has been set to true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">Symbol to remove</param>
|
||||||
|
public void RemoveTrackedSymbolAsync(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
SymbolTracker.RemoveTrackedSymbol(symbol);
|
||||||
|
|
||||||
|
((FuturesOrderTracker)Orders).ClearDataForSymbol(symbol);
|
||||||
|
((FuturesUserTradeTracker?)Trades)?.ClearDataForSymbol(symbol);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
@@ -51,17 +53,17 @@ namespace CryptoExchange.Net.Trackers.UserData
|
|||||||
|
|
||||||
var trackers = new List<UserDataItemTracker>();
|
var trackers = new List<UserDataItemTracker>();
|
||||||
|
|
||||||
var balanceTracker = new BalanceTracker(logger, balanceRestClient, balanceSocketClient, SharedAccountType.Spot, config.BalancesConfig, exchangeParameters);
|
var balanceTracker = new BalanceTracker(logger, SymbolTracker, balanceRestClient, balanceSocketClient, SharedAccountType.Spot, config.BalancesConfig, exchangeParameters);
|
||||||
Balances = balanceTracker;
|
Balances = balanceTracker;
|
||||||
trackers.Add(balanceTracker);
|
trackers.Add(balanceTracker);
|
||||||
|
|
||||||
var orderTracker = new SpotOrderTracker(logger, spotOrderRestClient, spotOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
var orderTracker = new SpotOrderTracker(logger, SymbolTracker, spotOrderRestClient, spotOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||||
Orders = orderTracker;
|
Orders = orderTracker;
|
||||||
trackers.Add(orderTracker);
|
trackers.Add(orderTracker);
|
||||||
|
|
||||||
if (config.TrackTrades)
|
if (config.TrackTrades)
|
||||||
{
|
{
|
||||||
var tradeTracker = new SpotUserTradeTracker(logger, spotOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
var tradeTracker = new SpotUserTradeTracker(logger, SymbolTracker, spotOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||||
Trades = tradeTracker;
|
Trades = tradeTracker;
|
||||||
trackers.Add(tradeTracker);
|
trackers.Add(tradeTracker);
|
||||||
|
|
||||||
@@ -91,10 +93,64 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add symbols to the list of symbols for which data is being tracked
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbols">Symbols to add</param>
|
||||||
|
public void AddTrackedSymbolsAsync(IEnumerable<SharedSymbol> symbols)
|
||||||
|
{
|
||||||
|
if (symbols.Any(x => x.TradingMode != TradingMode.Spot))
|
||||||
|
throw new ArgumentException("Futures symbol not allowed in spot tracker", nameof(symbols));
|
||||||
|
|
||||||
|
SymbolTracker.UpdateTrackedSymbols(symbols, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove a symbol from the list of symbols for which data is being tracked. Also removes stored data for that symbol.
|
||||||
|
/// Note that the symbol will be added again if new data for that symbol is received, unless the OnlyTrackProvidedSymbols option has been set to true.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">Symbol to remove</param>
|
||||||
|
public void RemoveTrackedSymbolAsync(SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
SymbolTracker.RemoveTrackedSymbol(symbol);
|
||||||
|
|
||||||
|
((SpotOrderTracker)Orders).ClearDataForSymbol(symbol);
|
||||||
|
((SpotUserTradeTracker?)Trades)?.ClearDataForSymbol(symbol);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,61 @@ 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.7.0 - 24 Feb 2026
|
||||||
|
* Added parsing of REST response data up to 128 characters for error responses
|
||||||
|
* Added check for invalid json in JsonSocketMessageHandler
|
||||||
|
* Added virtual GetTypeIdentifierNonJson for handling non-json messages in JsonSocketMessageHandler
|
||||||
|
* Added additional options to Rest client options for configuring HttpClient
|
||||||
|
* Updated INextPageToken parameter on Shared interfaces to PageRequest type, functionality unchanged
|
||||||
|
* Added SupportsAscending and SupportsDescending properties to PaginatedEndpointOptions to expose supported data directions
|
||||||
|
* Added MaxAge property to PaginatedEndpointOptions to expose the max age of data that can be requested
|
||||||
|
* Added Direction property to Shared interfaces paginated requests to configure pagination data direction
|
||||||
|
* Removed PaginationSupport property from PaginatedEndpointOptions, replaced by above new properties
|
||||||
|
* Updated Shared GetTradeHistoryRequest EndTime property to be optional
|
||||||
|
* Updated I(Futures/Spot)OrderRestClient.GetClosed(Futures/Spot)OrdersOptions from PaginatedEndpointOptions<GetClosedOrdersRequest> to GetClosedOrdersOptions
|
||||||
|
* Updated I(Futures/Spot)OrderRestClient.Get(Futures/Spot)UserTradesOptions from PaginatedEndpointOptions<GetUserTradesRequest> to GetUserTradesOptions
|
||||||
|
* Updated rate limiting PathStartFilter to ignore added or missing slash before the path
|
||||||
|
* Updated internal lock for subscription to ReaderWriterLockSlim on SocketConnection
|
||||||
|
* Removed check for OnlyTrackProvidedSymbols in combination with empty TrackedSymbols list
|
||||||
|
* Fixed KlineTracker throwing exception if there is no data in the initial snapshot
|
||||||
|
|
||||||
|
* Version 10.6.2 - 17 Feb 2026
|
||||||
|
* Fix for websocket queries which don't expects response getting stuck in subscribing state
|
||||||
|
|
||||||
|
* Version 10.6.1 - 16 Feb 2026
|
||||||
|
* Fixed exception when stopping SymbolOrderBook instance when update is received while closing
|
||||||
|
|
||||||
|
* Version 10.6.0 - 16 Feb 2026
|
||||||
|
* Updated symbol tracking logic on UserDataTracker, now is per UserDataTracker instead of per topic
|
||||||
|
* Added check for startTime filter for polling being to close to current time which can cause issues if time isn't in sync with server
|
||||||
|
* Added AddTrackedSymbolsAsync and RemoveTrackedSymbolAsync methods to UserDataTracker
|
||||||
|
* Added check SymbolOrderBook is still alive when trying to add updates to prevent unnoticed growing in the background when subscription isn't closed while book is
|
||||||
|
|
||||||
|
* Version 10.5.4 - 12 Feb 2026
|
||||||
|
* Fixed type check ExchangeParameters GetValue
|
||||||
|
* Fixed bug in polling time filter for UserDataTracker item
|
||||||
|
|
||||||
|
* Version 10.5.3 - 11 Feb 2026
|
||||||
|
* Fixed orders getting incorrectly set to canceled state for UserDataTracker spot and futures orders
|
||||||
|
* Added check EnumConverter to detect undefined int value parsing
|
||||||
|
|
||||||
|
* Version 10.5.2 - 10 Feb 2026
|
||||||
|
* Added check for subscribe queries with TimeoutBehavior.Success to complete when subscription has received update
|
||||||
|
* Added call to ApiClient.HandleUnhandledMessage when no websocket message processor is found based on topic to allow additional processing
|
||||||
|
* Combined websocket connection subscribe and re-subscribe logic
|
||||||
|
* Set websocket query completed after setting Result
|
||||||
|
|
||||||
|
* Version 10.5.1 - 10 Feb 2026
|
||||||
|
* Fixed trading mode selection for futures listen key methods in FuturesUserDataTracker
|
||||||
|
|
||||||
|
* Version 10.5.0 - 10 Feb 2026
|
||||||
|
* Added keep alive for listenkeys to UserDataTracker
|
||||||
|
* Updated logging unmatched websocket message
|
||||||
|
* Updated websocket message forwarding logic
|
||||||
|
* Fixed bug in IncomingKbps calculation
|
||||||
|
* Fixed bug in SendAsync in SocketConnection
|
||||||
|
* Fixed bug in UserDataTracker orders logic incorrectly setting order to canceled status
|
||||||
|
|
||||||
* Version 10.4.1 - 06 Feb 2026
|
* Version 10.4.1 - 06 Feb 2026
|
||||||
* Updated UserDataTracker to only track symbol when position size > 0
|
* Updated UserDataTracker to only track symbol when position size > 0
|
||||||
* Update UserDataTracker log verbosity
|
* Update UserDataTracker log verbosity
|
||||||
|
|||||||
Reference in New Issue
Block a user