mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87dd2d9d40 | |||
| f2b0cb2f0d | |||
| de0a954a91 | |||
| 4a79ce22ec | |||
| 40d480e1fc | |||
| 74e5cf6fc9 | |||
| 2fd3912795 | |||
| ec44307a0c | |||
| ba55705385 | |||
| 2c63a83117 | |||
| 71b1e5e906 | |||
| eaeba6f27e |
@@ -4,6 +4,7 @@ using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
@@ -139,5 +140,17 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
ClassicAssert.False(result1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CancellingWait_Should_ReturnFalse()
|
||||
{
|
||||
var evnt = new AsyncResetEvent(false, true);
|
||||
|
||||
var waiter1 = evnt.WaitAsync(ct: new CancellationTokenSource(50).Token);
|
||||
|
||||
var result1 = await waiter1;
|
||||
|
||||
ClassicAssert.False(result1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,11 +518,14 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
protected DateTime GetTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
|
||||
{
|
||||
var result = TimeProvider.GetTime().Add(TimeOffsetManager.GetSocketOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
|
||||
if (includeOneSecondOffset)
|
||||
result = result.AddSeconds(-1);
|
||||
var timestamp = TimeProvider.GetTime();
|
||||
if(apiClient.ApiOptions.AutoTimestamp ?? apiClient.ClientOptions.AutoTimestamp)
|
||||
timestamp = timestamp.Add(-TimeOffsetManager.GetSocketOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
|
||||
|
||||
return result;
|
||||
if (includeOneSecondOffset)
|
||||
timestamp = timestamp.AddSeconds(-1);
|
||||
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -13,7 +13,10 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||
{
|
||||
private string? _clientName;
|
||||
/// <summary>
|
||||
/// Client name
|
||||
/// </summary>
|
||||
protected string? _clientName;
|
||||
|
||||
/// <summary>
|
||||
/// Logger
|
||||
|
||||
@@ -39,6 +39,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
public string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether client is disposed
|
||||
/// </summary>
|
||||
public bool Disposed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
@@ -125,6 +130,8 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Disposed = true;
|
||||
|
||||
foreach (var client in ApiClients)
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>10.3.1</PackageVersion>
|
||||
<AssemblyVersion>10.3.1</AssemblyVersion>
|
||||
<FileVersion>10.3.1</FileVersion>
|
||||
<PackageVersion>10.4.1</PackageVersion>
|
||||
<AssemblyVersion>10.4.1</AssemblyVersion>
|
||||
<FileVersion>10.4.1</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
@@ -32,6 +32,66 @@ namespace CryptoExchange.Net
|
||||
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the specific topic has been cached
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id</param>
|
||||
public static bool HasCached(string topicId)
|
||||
{
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
return false;
|
||||
|
||||
return exchangeInfo.Symbols.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a specific exchange(topic) support the provided symbol
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="symbolName">The symbol name</param>
|
||||
public static bool SupportsSymbol(string topicId, string symbolName)
|
||||
{
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
return false;
|
||||
|
||||
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a specific exchange(topic) support the provided symbol
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="symbol">The symbol info</param>
|
||||
public static bool SupportsSymbol(string topicId, SharedSymbol symbol)
|
||||
{
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
return false;
|
||||
|
||||
return exchangeInfo.Symbols.Any(x =>
|
||||
x.Value.TradingMode == symbol.TradingMode
|
||||
&& x.Value.BaseAsset == symbol.BaseAsset
|
||||
&& x.Value.QuoteAsset == symbol.QuoteAsset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all symbols for a specific base asset
|
||||
/// </summary>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="baseAsset">Base asset name</param>
|
||||
public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string baseAsset)
|
||||
{
|
||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||
return [];
|
||||
|
||||
return exchangeInfo.Symbols
|
||||
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(x => x.Value)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a symbol name to a SharedSymbol
|
||||
/// </summary>
|
||||
|
||||
@@ -22,5 +22,10 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// The exchange name
|
||||
/// </summary>
|
||||
string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether client is disposed
|
||||
/// </summary>
|
||||
bool Disposed { get; }
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,11 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// </summary>
|
||||
public int CurrentSubscriptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether client is disposed
|
||||
/// </summary>
|
||||
bool Disposed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from a stream using the subscription id received when starting the subscription
|
||||
/// </summary>
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Async auto reset based on Stephen Toub`s implementation
|
||||
/// https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-2-asyncautoresetevent/
|
||||
/// </summary>
|
||||
public class AsyncResetEvent : IDisposable
|
||||
{
|
||||
private static readonly Task<bool> _completed = Task.FromResult(true);
|
||||
private Queue<TaskCompletionSource<bool>> _waits = new Queue<TaskCompletionSource<bool>>();
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly Lock _waitsLock = new Lock();
|
||||
#else
|
||||
private readonly object _waitsLock = new object();
|
||||
#endif
|
||||
private bool _signaled;
|
||||
private readonly bool _reset;
|
||||
|
||||
/// <summary>
|
||||
/// New AsyncResetEvent
|
||||
/// </summary>
|
||||
/// <param name="initialState"></param>
|
||||
/// <param name="reset"></param>
|
||||
public AsyncResetEvent(bool initialState = false, bool reset = true)
|
||||
{
|
||||
_signaled = initialState;
|
||||
_reset = reset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for the AutoResetEvent to be set
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
|
||||
{
|
||||
CancellationTokenRegistration registration = default;
|
||||
try
|
||||
{
|
||||
Task<bool> waiter = _completed;
|
||||
lock (_waitsLock)
|
||||
{
|
||||
if (_signaled)
|
||||
{
|
||||
if (_reset)
|
||||
_signaled = false;
|
||||
}
|
||||
else if (!ct.IsCancellationRequested)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
if (timeout.HasValue)
|
||||
{
|
||||
var timeoutSource = new CancellationTokenSource(timeout.Value);
|
||||
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct);
|
||||
ct = cancellationSource.Token;
|
||||
}
|
||||
|
||||
registration = ct.Register(() =>
|
||||
{
|
||||
lock (_waitsLock)
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
|
||||
// Not the cleanest but it works
|
||||
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
||||
}
|
||||
}, useSynchronizationContext: false);
|
||||
|
||||
|
||||
_waits.Enqueue(tcs);
|
||||
waiter = tcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
return await waiter.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
registration.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signal a waiter
|
||||
/// </summary>
|
||||
public void Set()
|
||||
{
|
||||
lock (_waitsLock)
|
||||
{
|
||||
if (!_reset)
|
||||
{
|
||||
// Act as ManualResetEvent. Once set keep it signaled and signal everyone who is waiting
|
||||
_signaled = true;
|
||||
while (_waits.Count > 0)
|
||||
{
|
||||
var toRelease = _waits.Dequeue();
|
||||
toRelease.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Act as AutoResetEvent. When set signal 1 waiter
|
||||
if (_waits.Count > 0)
|
||||
{
|
||||
var toRelease = _waits.Dequeue();
|
||||
toRelease.TrySetResult(true);
|
||||
}
|
||||
else if (!_signaled)
|
||||
{
|
||||
_signaled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_waits.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Async auto/manual reset event implementation
|
||||
/// </summary>
|
||||
public class AsyncResetEvent
|
||||
{
|
||||
private readonly Queue<TaskCompletionSource<bool>> _waiters = new();
|
||||
private readonly bool _autoReset;
|
||||
private bool _signaled;
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly Lock _waitersLock = new Lock();
|
||||
#else
|
||||
private readonly object _waitersLock = new object();
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public AsyncResetEvent(bool initialState = false, bool autoReset = true)
|
||||
{
|
||||
_signaled = initialState;
|
||||
_autoReset = autoReset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for the set event
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> WaitAsync(
|
||||
TimeSpan? timeout = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
TaskCompletionSource<bool> tcs;
|
||||
|
||||
lock (_waitersLock)
|
||||
{
|
||||
if (_signaled)
|
||||
{
|
||||
// Already was signaled, can return immediately
|
||||
if (_autoReset)
|
||||
_signaled = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_waiters.Enqueue(tcs);
|
||||
}
|
||||
|
||||
CancellationTokenSource? delayCts = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (timeout.HasValue || ct.CanBeCanceled)
|
||||
{
|
||||
delayCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
|
||||
var delayTask = Task.Delay(
|
||||
timeout ?? Timeout.InfiniteTimeSpan,
|
||||
delayCts.Token);
|
||||
|
||||
var completedTask =
|
||||
await Task.WhenAny(tcs.Task, delayTask)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (completedTask != tcs.Task)
|
||||
{
|
||||
// This was a timeout or cancellation, need to remove tcs from waiters
|
||||
// if the tcs was set instead it will be removed in the Set method
|
||||
if (tcs.TrySetResult(false))
|
||||
{
|
||||
lock (_waitersLock)
|
||||
{
|
||||
// Dequeue and put in the back of the queue again except for the one we need to remove
|
||||
int count = _waiters.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var w = _waiters.Dequeue();
|
||||
if (w != tcs)
|
||||
_waiters.Enqueue(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Actively stop the delay if tcs.Task won
|
||||
delayCts?.Cancel();
|
||||
delayCts?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signal a waiter
|
||||
/// </summary>
|
||||
public void Set()
|
||||
{
|
||||
if (!_autoReset && _signaled)
|
||||
// Already signaled and not resetting
|
||||
return;
|
||||
|
||||
lock (_waitersLock)
|
||||
{
|
||||
if (_autoReset)
|
||||
{
|
||||
while (_waiters.Count > 0)
|
||||
{
|
||||
// Try to dequeue and set the result
|
||||
// If result setting was not successful it means timeout/cancellation happened at the same time
|
||||
// If this is the case this Set isn't the one setting the result and we need to continue
|
||||
var w = _waiters.Dequeue();
|
||||
if (w.TrySetResult(true))
|
||||
return;
|
||||
}
|
||||
|
||||
// No queued waiters, set signaled for next waiter
|
||||
_signaled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_signaled = true;
|
||||
|
||||
// Signal all current waiters
|
||||
while (_waiters.Count > 0)
|
||||
{
|
||||
var w = _waiters.Dequeue();
|
||||
w.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,11 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
public class ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not to automatically sync the local time with the server time
|
||||
/// </summary>
|
||||
public bool? AutoTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects will also include the originally received string data in the OriginalData property.
|
||||
/// Note that this comes at a performance cost
|
||||
|
||||
@@ -8,6 +8,10 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
public class ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not to automatically sync the local time with the server time
|
||||
/// </summary>
|
||||
public bool AutoTimestamp { get; set; }
|
||||
/// <summary>
|
||||
/// Proxy settings
|
||||
/// </summary>
|
||||
|
||||
@@ -8,11 +8,6 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
public class RestApiOptions : ApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not to automatically sync the local time with the server time
|
||||
/// </summary>
|
||||
public bool? AutoTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||
/// </summary>
|
||||
|
||||
@@ -8,11 +8,6 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
public class RestExchangeOptions: ExchangeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not to automatically sync the local time with the server time
|
||||
/// </summary>
|
||||
public bool AutoTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How often the timestamp adjustment between client and server is recalculated. If you need a very small TimeSpan here you're probably better of syncing your server time more often
|
||||
/// </summary>
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.MaxSocketConnections = MaxSocketConnections;
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,15 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </remarks>
|
||||
public int? ReceiveBufferSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SocketExchangeOptions()
|
||||
{
|
||||
// Enable auto timestamping by default for sockets
|
||||
AutoTimestamp = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// </summary>
|
||||
@@ -83,6 +92,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public T Set<T>(T item) where T : SocketExchangeOptions, new()
|
||||
{
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.ReconnectPolicy = ReconnectPolicy;
|
||||
item.DelayAfterConnect = DelayAfterConnect;
|
||||
|
||||
@@ -109,6 +109,21 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public int Id => _subscription.Id;
|
||||
|
||||
/// <summary>
|
||||
/// The last timestamp anything was received from the server
|
||||
/// </summary>
|
||||
public DateTime? LastReceiveTime => _connection.LastReceiveTime;
|
||||
|
||||
/// <summary>
|
||||
/// The current websocket status
|
||||
/// </summary>
|
||||
public SocketStatus SocketStatus => _connection.Status;
|
||||
|
||||
/// <summary>
|
||||
/// The current subscription status
|
||||
/// </summary>
|
||||
public SubscriptionStatus SubscriptionStatus => _subscription.Status;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Transfer status
|
||||
/// </summary>
|
||||
public enum SharedTransferStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// In progress
|
||||
/// </summary>
|
||||
InProgress,
|
||||
/// <summary>
|
||||
/// Failed
|
||||
/// </summary>
|
||||
Failed,
|
||||
/// <summary>
|
||||
/// Completed
|
||||
/// </summary>
|
||||
Completed
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,25 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Futures symbol request options
|
||||
/// </summary>
|
||||
EndpointOptions<GetSymbolsRequest> GetFuturesSymbolsOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get all futures symbols for a specific base asset
|
||||
/// </summary>
|
||||
/// <param name="baseAsset">Asset, for example `ETH`</param>
|
||||
Task<ExchangeResult<SharedSymbol[]>> GetFuturesSymbolsForBaseAssetAsync(string baseAsset);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the client supports a futures symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol</param>
|
||||
Task<ExchangeResult<bool>> SupportsFuturesSymbolAsync(SharedSymbol symbol);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the client supports a futures symbol
|
||||
/// </summary>
|
||||
/// <param name="symbolName">The symbol name</param>
|
||||
Task<ExchangeResult<bool>> SupportsFuturesSymbolAsync(string symbolName);
|
||||
|
||||
/// <summary>
|
||||
/// Get info on all futures symbols supported on the exchange
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
@@ -13,6 +14,24 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
EndpointOptions<GetSymbolsRequest> GetSpotSymbolsOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get all spot symbols for a specific base asset
|
||||
/// </summary>
|
||||
/// <param name="baseAsset">Asset, for example `ETH`</param>
|
||||
Task<ExchangeResult<SharedSymbol[]>> GetSpotSymbolsForBaseAssetAsync(string baseAsset);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the client supports a spot symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol</param>
|
||||
Task<ExchangeResult<bool>> SupportsSpotSymbolAsync(SharedSymbol symbol);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the client supports a spot symbol
|
||||
/// </summary>
|
||||
/// <param name="symbolName">The symbol name</param>
|
||||
Task<ExchangeResult<bool>> SupportsSpotSymbolAsync(string symbolName);
|
||||
|
||||
/// <summary>
|
||||
/// Get info on all available spot symbols on the exchange
|
||||
/// </summary>
|
||||
|
||||
@@ -38,6 +38,17 @@ namespace CryptoExchange.Net.SharedApis
|
||||
Exchange = exchange;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ExchangeResult(
|
||||
string exchange,
|
||||
T result) :
|
||||
base(result, null, null)
|
||||
{
|
||||
Exchange = exchange;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => $"{Exchange} - " + base.ToString();
|
||||
}
|
||||
|
||||
@@ -44,15 +44,21 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public bool Completed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Status of the deposit
|
||||
/// </summary>
|
||||
public SharedTransferStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedDeposit(string asset, decimal quantity, bool completed, DateTime timestamp)
|
||||
public SharedDeposit(string asset, decimal quantity, bool completed, DateTime timestamp, SharedTransferStatus status)
|
||||
{
|
||||
Asset = asset;
|
||||
Quantity = quantity;
|
||||
Timestamp = timestamp;
|
||||
Completed = completed;
|
||||
Status = status;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public SharedPositionSide PositionSide { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the position is one way mode
|
||||
/// </summary>
|
||||
public SharedPositionMode PositionMode { get; set; }
|
||||
/// <summary>
|
||||
/// Average open price
|
||||
/// </summary>
|
||||
public decimal? AverageOpenPrice { get; set; }
|
||||
|
||||
@@ -21,6 +21,11 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public decimal? QuantityInContracts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether all values are null or zero
|
||||
/// </summary>
|
||||
public bool IsZero => !(QuantityInBaseAsset > 0) && !(QuantityInQuoteAsset > 0) && !(QuantityInContracts > 0);
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@@ -49,7 +49,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
private int _reconnectAttempt;
|
||||
private readonly int _receiveBufferSize;
|
||||
|
||||
private const int _defaultReceiveBufferSize = 1048576;
|
||||
private const int _sendBufferSize = 4096;
|
||||
|
||||
private int _bytesReceived = 0;
|
||||
@@ -71,7 +70,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
/// <summary>
|
||||
/// The timestamp this socket has been active for the last time
|
||||
/// </summary>
|
||||
public DateTime LastActionTime { get; private set; }
|
||||
public DateTime? LastReceiveTime { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Uri Uri => Parameters.Uri;
|
||||
@@ -487,7 +486,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
_disposed = true;
|
||||
_socket.Dispose();
|
||||
_ctsSource?.Dispose();
|
||||
_sendEvent.Dispose();
|
||||
_logger.SocketDisposed(Id);
|
||||
}
|
||||
|
||||
@@ -623,6 +621,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
break;
|
||||
}
|
||||
|
||||
LastReceiveTime = DateTime.UtcNow;
|
||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
// Connection closed
|
||||
@@ -773,6 +772,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
break;
|
||||
}
|
||||
|
||||
LastReceiveTime = DateTime.UtcNow;
|
||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
// Connection closed
|
||||
@@ -881,7 +881,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
/// <returns></returns>
|
||||
protected void ProcessDataNew(WebSocketMessageType type, ReadOnlySpan<byte> data)
|
||||
{
|
||||
LastActionTime = DateTime.UtcNow;
|
||||
_connection.HandleStreamMessage2(type, data);
|
||||
}
|
||||
|
||||
@@ -892,7 +891,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
protected async Task CheckTimeoutAsync()
|
||||
{
|
||||
_logger.SocketStartingTaskForNoDataReceivedCheck(Id, Parameters.Timeout);
|
||||
LastActionTime = DateTime.UtcNow;
|
||||
LastReceiveTime = DateTime.UtcNow;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
@@ -900,7 +899,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
if (_ctsSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (DateTime.UtcNow - LastActionTime > Parameters.Timeout)
|
||||
if (DateTime.UtcNow - LastReceiveTime > Parameters.Timeout)
|
||||
{
|
||||
_logger.SocketNoDataReceiveTimoutReconnect(Id, Parameters.Timeout);
|
||||
_ = ReconnectAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -69,6 +69,10 @@ namespace CryptoExchange.Net.Sockets.Default.Interfaces
|
||||
/// </summary>
|
||||
bool IsOpen { get; }
|
||||
/// <summary>
|
||||
/// Last timestamp something was received from the server
|
||||
/// </summary>
|
||||
DateTime? LastReceiveTime { get; }
|
||||
/// <summary>
|
||||
/// Connect the socket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
|
||||
@@ -178,6 +178,11 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
/// </summary>
|
||||
public DateTime? DisconnectTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Last timestamp something was received from the server
|
||||
/// </summary>
|
||||
public DateTime? LastReceiveTime => _socket.LastReceiveTime;
|
||||
|
||||
/// <summary>
|
||||
/// Tag for identification
|
||||
/// </summary>
|
||||
@@ -787,7 +792,6 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
{
|
||||
Status = SocketStatus.Disposed;
|
||||
periodicEvent?.Set();
|
||||
periodicEvent?.Dispose();
|
||||
_socket.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Sockets.Default
|
||||
return;
|
||||
|
||||
_status = value;
|
||||
Task.Run(() => StatusChanged?.Invoke(value));
|
||||
StatusChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -233,7 +233,6 @@ namespace CryptoExchange.Net.Sockets.HighPerf
|
||||
{
|
||||
Status = SocketStatus.Disposed;
|
||||
periodicEvent?.Set();
|
||||
periodicEvent?.Dispose();
|
||||
_socket.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace CryptoExchange.Net.Testing.Implementations
|
||||
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||
|
||||
public static int lastId = 0;
|
||||
public DateTime? LastReceiveTime { get; }
|
||||
#if NET9_0_OR_GREATER
|
||||
public static readonly Lock lastIdLock = new Lock();
|
||||
#else
|
||||
|
||||
@@ -55,6 +55,11 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
/// </summary>
|
||||
SharedKline? Last { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The kline interval
|
||||
/// </summary>
|
||||
public SharedKlineInterval Interval { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event for when a new kline is added
|
||||
/// </summary>
|
||||
|
||||
@@ -45,10 +45,6 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
/// </summary>
|
||||
protected bool _changed = false;
|
||||
/// <summary>
|
||||
/// The kline interval
|
||||
/// </summary>
|
||||
protected readonly SharedKlineInterval _interval;
|
||||
/// <summary>
|
||||
/// Whether the snapshot has been set
|
||||
/// </summary>
|
||||
protected bool _snapshotSet;
|
||||
@@ -66,6 +62,10 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
/// </summary>
|
||||
protected DateTime? _firstTimestamp;
|
||||
|
||||
/// <summary>
|
||||
/// The kline interval
|
||||
/// </summary>
|
||||
public SharedKlineInterval Interval { get; }
|
||||
/// <inheritdoc/>
|
||||
public SyncStatus Status
|
||||
{
|
||||
@@ -165,7 +165,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
Exchange = restClient.Exchange;
|
||||
Limit = limit;
|
||||
Period = period;
|
||||
_interval = interval;
|
||||
Interval = interval;
|
||||
_socketClient = socketClient;
|
||||
_restClient = restClient;
|
||||
}
|
||||
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
Status = SyncStatus.Syncing;
|
||||
_logger.KlineTrackerStarting(SymbolName);
|
||||
|
||||
var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, _interval),
|
||||
var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, Interval),
|
||||
update =>
|
||||
{
|
||||
AddOrUpdate(update.Data);
|
||||
@@ -237,7 +237,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
|
||||
var limit = Math.Min(_restClient.GetKlinesOptions.MaxLimit, Limit ?? 100);
|
||||
|
||||
var request = new GetKlinesRequest(Symbol, _interval, startTime, DateTime.UtcNow, limit: limit);
|
||||
var request = new GetKlinesRequest(Symbol, Interval, startTime, DateTime.UtcNow, limit: limit);
|
||||
var data = new List<SharedKline>();
|
||||
await foreach (var result in ExchangeHelpers.ExecutePages(_restClient.GetKlinesAsync, request).ConfigureAwait(false))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Data tracker interface
|
||||
/// </summary>
|
||||
public interface IUserDataTracker<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the tracker is currently fully connected
|
||||
/// </summary>
|
||||
bool Connected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Currently tracked symbols. Data for these symbols will be requested when polling.
|
||||
/// Websocket updates will be available for all symbols regardless.
|
||||
/// When new data is received for a symbol which is not yet being tracked it will be added to this list and polled in the future unless the `OnlyTrackProvidedSymbols` option is set in the configuration.
|
||||
/// </summary>
|
||||
IEnumerable<SharedSymbol> TrackedSymbols { get; }
|
||||
|
||||
/// <summary>
|
||||
/// On connection status change. Might trigger multiple times with the same status depending on the underlying subscriptions.
|
||||
/// </summary>
|
||||
event Action<bool>? OnConnectedChange;
|
||||
|
||||
/// <summary>
|
||||
/// Currently tracker values
|
||||
/// </summary>
|
||||
T[] Values { get; }
|
||||
|
||||
/// <summary>
|
||||
/// On data update
|
||||
/// </summary>
|
||||
event Func<UserDataUpdate<T[]>, Task>? OnUpdate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Futures user data tracker
|
||||
/// </summary>
|
||||
public interface IUserFuturesDataTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// User identifier
|
||||
/// </summary>
|
||||
string? UserIdentifier { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the tracker is currently fully connected
|
||||
/// </summary>
|
||||
bool Connected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
public string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Balances tracker
|
||||
/// </summary>
|
||||
IUserDataTracker<SharedBalance> Balances { get; }
|
||||
/// <summary>
|
||||
/// Orders tracker
|
||||
/// </summary>
|
||||
IUserDataTracker<SharedFuturesOrder> Orders { get; }
|
||||
/// <summary>
|
||||
/// Positions tracker
|
||||
/// </summary>
|
||||
IUserDataTracker<SharedPosition> Positions { get; }
|
||||
/// <summary>
|
||||
/// Trades tracker
|
||||
/// </summary>
|
||||
IUserDataTracker<SharedUserTrade>? Trades { get; }
|
||||
|
||||
/// <summary>
|
||||
/// On connection status change
|
||||
/// </summary>
|
||||
event Action<UserDataType, bool>? OnConnectedChange;
|
||||
|
||||
/// <summary>
|
||||
/// Start tracking user data
|
||||
/// </summary>
|
||||
Task<CallResult> StartAsync();
|
||||
/// <summary>
|
||||
/// Stop tracking data
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task StopAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// User data tracker
|
||||
/// </summary>
|
||||
public interface IUserSpotDataTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// User identifier
|
||||
/// </summary>
|
||||
string? UserIdentifier { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the tracker is currently fully connected
|
||||
/// </summary>
|
||||
bool Connected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
public string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Balances tracker
|
||||
/// </summary>
|
||||
IUserDataTracker<SharedBalance> Balances { get; }
|
||||
/// <summary>
|
||||
/// Orders tracker
|
||||
/// </summary>
|
||||
IUserDataTracker<SharedSpotOrder> Orders { get; }
|
||||
/// <summary>
|
||||
/// Trades tracker
|
||||
/// </summary>
|
||||
IUserDataTracker<SharedUserTrade>? Trades { get; }
|
||||
|
||||
/// <summary>
|
||||
/// On connection status change
|
||||
/// </summary>
|
||||
event Action<UserDataType, bool>? OnConnectedChange;
|
||||
|
||||
/// <summary>
|
||||
/// Start tracking user data
|
||||
/// </summary>
|
||||
Task<CallResult> StartAsync();
|
||||
/// <summary>
|
||||
/// Stop tracking data
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task StopAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
{
|
||||
/// <summary>
|
||||
/// Balance tracker implementation
|
||||
/// </summary>
|
||||
public class BalanceTracker : UserDataItemTracker<SharedBalance>
|
||||
{
|
||||
private readonly IBalanceRestClient _restClient;
|
||||
private readonly IBalanceSocketClient? _socketClient;
|
||||
private readonly ExchangeParameters? _exchangeParameters;
|
||||
private readonly SharedAccountType _accountType;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public BalanceTracker(
|
||||
ILogger logger,
|
||||
IBalanceRestClient restClient,
|
||||
IBalanceSocketClient? socketClient,
|
||||
SharedAccountType accountType,
|
||||
TrackerItemConfig config,
|
||||
ExchangeParameters? exchangeParameters = null
|
||||
) : base(logger, UserDataType.Balances, restClient.Exchange, config, false, null)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||
|
||||
_restClient = restClient;
|
||||
_socketClient = socketClient;
|
||||
_exchangeParameters = exchangeParameters;
|
||||
_accountType = accountType;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool Update(SharedBalance existingItem, SharedBalance updateItem)
|
||||
{
|
||||
var changed = false;
|
||||
if (existingItem.Total != updateItem.Total)
|
||||
{
|
||||
existingItem.Total = updateItem.Total;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (existingItem.Available != updateItem.Available)
|
||||
{
|
||||
existingItem.Available = updateItem.Available;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetKey(SharedBalance item) => item.Asset + item.IsolatedMarginSymbol;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool? CheckIfUpdateShouldBeApplied(SharedBalance existingItem, SharedBalance updateItem) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
||||
|
||||
var accountType = _accountType == SharedAccountType.Spot ? TradingMode.Spot :
|
||||
_accountType == SharedAccountType.PerpetualInverseFutures ? TradingMode.PerpetualInverse :
|
||||
_accountType == SharedAccountType.DeliveryLinearFutures ? TradingMode.DeliveryLinear :
|
||||
_accountType == SharedAccountType.DeliveryInverseFutures ? TradingMode.DeliveryInverse :
|
||||
TradingMode.PerpetualLinear;
|
||||
return ExchangeHelpers.ProcessQueuedAsync<SharedBalance[]>(
|
||||
async handler => await _socketClient.SubscribeToBalanceUpdatesAsync(new SubscribeBalancesRequest(listenKey, accountType, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
||||
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<bool> DoPollAsync()
|
||||
{
|
||||
var balances = await _restClient.GetBalancesAsync(new GetBalancesRequest(accountType: _accountType, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (balances.Success)
|
||||
await HandleUpdateAsync(UpdateSource.Poll, balances.Data).ConfigureAwait(false);
|
||||
else
|
||||
_initialPollingError ??= balances.Error;
|
||||
|
||||
return !balances.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
{
|
||||
/// <summary>
|
||||
/// Futures order tracker
|
||||
/// </summary>
|
||||
public class FuturesOrderTracker : UserDataItemTracker<SharedFuturesOrder>
|
||||
{
|
||||
private readonly IFuturesOrderRestClient _restClient;
|
||||
private readonly IFuturesOrderSocketClient? _socketClient;
|
||||
private readonly ExchangeParameters? _exchangeParameters;
|
||||
private readonly bool _requiresSymbolParameterOpenOrders;
|
||||
|
||||
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public FuturesOrderTracker(
|
||||
ILogger logger,
|
||||
IFuturesOrderRestClient restClient,
|
||||
IFuturesOrderSocketClient? socketClient,
|
||||
TrackerItemConfig config,
|
||||
IEnumerable<SharedSymbol> symbols,
|
||||
bool onlyTrackProvidedSymbols,
|
||||
ExchangeParameters? exchangeParameters = null
|
||||
) : base(logger, UserDataType.Orders, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||
|
||||
_restClient = restClient;
|
||||
_socketClient = socketClient;
|
||||
_exchangeParameters = exchangeParameters;
|
||||
|
||||
_requiresSymbolParameterOpenOrders = restClient.GetOpenFuturesOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool Update(SharedFuturesOrder existingItem, SharedFuturesOrder updateItem)
|
||||
{
|
||||
var changed = false;
|
||||
if (updateItem.AveragePrice != null && updateItem.AveragePrice != existingItem.AveragePrice)
|
||||
{
|
||||
existingItem.AveragePrice = updateItem.AveragePrice;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.OrderPrice != null && updateItem.OrderPrice != existingItem.OrderPrice)
|
||||
{
|
||||
existingItem.OrderPrice = updateItem.OrderPrice;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.Fee != null && updateItem.Fee != existingItem.Fee)
|
||||
{
|
||||
existingItem.Fee = updateItem.Fee;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.FeeAsset != null && updateItem.FeeAsset != existingItem.FeeAsset)
|
||||
{
|
||||
existingItem.FeeAsset = updateItem.FeeAsset;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.OrderQuantity != null && updateItem.OrderQuantity != existingItem.OrderQuantity)
|
||||
{
|
||||
existingItem.OrderQuantity = updateItem.OrderQuantity;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.QuantityFilled != null && updateItem.QuantityFilled != existingItem.QuantityFilled)
|
||||
{
|
||||
existingItem.QuantityFilled = updateItem.QuantityFilled;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.Status != existingItem.Status)
|
||||
{
|
||||
existingItem.Status = updateItem.Status;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.StopLossPrice != existingItem.StopLossPrice)
|
||||
{
|
||||
existingItem.StopLossPrice = updateItem.StopLossPrice;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.TakeProfitPrice != existingItem.TakeProfitPrice)
|
||||
{
|
||||
existingItem.TakeProfitPrice = updateItem.TakeProfitPrice;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.TriggerPrice != existingItem.TriggerPrice)
|
||||
{
|
||||
existingItem.TriggerPrice = updateItem.TriggerPrice;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.UpdateTime != null && updateItem.UpdateTime != existingItem.UpdateTime)
|
||||
{
|
||||
existingItem.UpdateTime = updateItem.UpdateTime;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetKey(SharedFuturesOrder item) => item.OrderId;
|
||||
/// <inheritdoc />
|
||||
protected override TimeSpan GetAge(DateTime time, SharedFuturesOrder item) => item.Status == SharedOrderStatus.Open ? TimeSpan.Zero : time - (item.UpdateTime ?? item.CreateTime ?? time);
|
||||
/// <inheritdoc />
|
||||
protected override bool? CheckIfUpdateShouldBeApplied(SharedFuturesOrder existingItem, SharedFuturesOrder updateItem)
|
||||
{
|
||||
if (existingItem.Status == SharedOrderStatus.Open && updateItem.Status != SharedOrderStatus.Open)
|
||||
// status changed from open to not open
|
||||
return true;
|
||||
|
||||
if (existingItem.Status != SharedOrderStatus.Open && updateItem.Status == SharedOrderStatus.Open)
|
||||
// status changed from not open to open; stale
|
||||
return false;
|
||||
|
||||
if (existingItem.UpdateTime != null && updateItem.UpdateTime != null)
|
||||
{
|
||||
// If both have an update time base of that
|
||||
if (existingItem.UpdateTime < updateItem.UpdateTime)
|
||||
return true;
|
||||
|
||||
if (existingItem.UpdateTime > updateItem.UpdateTime)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existingItem.QuantityFilled != null && updateItem.QuantityFilled != null)
|
||||
{
|
||||
if (existingItem.QuantityFilled.QuantityInBaseAsset != null && updateItem.QuantityFilled.QuantityInBaseAsset != null)
|
||||
{
|
||||
// If base quantity is not null we can base it on that
|
||||
if (existingItem.QuantityFilled.QuantityInBaseAsset < updateItem.QuantityFilled.QuantityInBaseAsset)
|
||||
return true;
|
||||
|
||||
else if (existingItem.QuantityFilled.QuantityInBaseAsset > updateItem.QuantityFilled.QuantityInBaseAsset)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existingItem.QuantityFilled.QuantityInQuoteAsset != null && updateItem.QuantityFilled.QuantityInQuoteAsset != null)
|
||||
{
|
||||
// If quote quantity is not null we can base it on that
|
||||
if (existingItem.QuantityFilled.QuantityInQuoteAsset < updateItem.QuantityFilled.QuantityInQuoteAsset)
|
||||
return true;
|
||||
|
||||
else if (existingItem.QuantityFilled.QuantityInQuoteAsset > updateItem.QuantityFilled.QuantityInQuoteAsset)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingItem.Fee != null && updateItem.Fee != null)
|
||||
{
|
||||
// Higher fee means later processing
|
||||
if (existingItem.Fee < updateItem.Fee)
|
||||
return true;
|
||||
|
||||
if (existingItem.Fee > updateItem.Fee)
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override async Task HandleUpdateAsync(UpdateSource source, SharedFuturesOrder[] @event)
|
||||
{
|
||||
await base.HandleUpdateAsync(source, @event).ConfigureAwait(false);
|
||||
|
||||
var trades = @event.Where(x => x.LastTrade != null).Select(x => x.LastTrade!).ToArray();
|
||||
if (trades.Length != 0 && OnTradeUpdate != null)
|
||||
await OnTradeUpdate.Invoke(source, trades).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
||||
|
||||
return ExchangeHelpers.ProcessQueuedAsync<SharedFuturesOrder[]>(
|
||||
async handler => await _socketClient.SubscribeToFuturesOrderUpdatesAsync(new SubscribeFuturesOrderRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
||||
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<bool> DoPollAsync()
|
||||
{
|
||||
var anyError = false;
|
||||
List<SharedFuturesOrder> openOrders = new List<SharedFuturesOrder>();
|
||||
|
||||
if (!_requiresSymbolParameterOpenOrders)
|
||||
{
|
||||
var openOrdersResult = await _restClient.GetOpenFuturesOrdersAsync(new GetOpenOrdersRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!openOrdersResult.Success)
|
||||
{
|
||||
anyError = true;
|
||||
|
||||
_initialPollingError ??= openOrdersResult.Error;
|
||||
if (!_firstPollDone)
|
||||
return anyError;
|
||||
}
|
||||
else
|
||||
{
|
||||
openOrders.AddRange(openOrdersResult.Data);
|
||||
await HandleUpdateAsync(UpdateSource.Poll, openOrdersResult.Data).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var symbol in _symbols.ToList())
|
||||
{
|
||||
var openOrdersResult = await _restClient.GetOpenFuturesOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!openOrdersResult.Success)
|
||||
{
|
||||
anyError = true;
|
||||
|
||||
_initialPollingError ??= openOrdersResult.Error;
|
||||
if (!_firstPollDone)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
openOrders.AddRange(openOrdersResult.Data);
|
||||
await HandleUpdateAsync(UpdateSource.Poll, openOrdersResult.Data).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var symbol in _symbols.ToList())
|
||||
{
|
||||
var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
||||
var updatedPollTime = DateTime.UtcNow;
|
||||
var closedOrdersResult = await _restClient.GetClosedFuturesOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!closedOrdersResult.Success)
|
||||
{
|
||||
anyError = true;
|
||||
|
||||
_initialPollingError ??= closedOrdersResult.Error;
|
||||
if (!_firstPollDone)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastDataTimeBeforeDisconnect = null;
|
||||
_lastPollTime = updatedPollTime;
|
||||
|
||||
// Filter orders to only include where close time is after the start time
|
||||
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
||||
x.UpdateTime != null && x.UpdateTime >= _startTime // Updated after the tracker start time
|
||||
|| x.CreateTime != null && x.CreateTime >= _startTime // Created after the tracker start time
|
||||
|| x.CreateTime == null && x.UpdateTime == null // Unknown time
|
||||
).ToArray();
|
||||
|
||||
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
||||
var openOrdersNotReturned = Values.Where(x =>
|
||||
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset // Orders for the same symbol
|
||||
&& x.QuantityFilled?.IsZero == true // With no filled value
|
||||
&& !openOrders.Any(r => r.OrderId == x.OrderId) // Not returned in open orders
|
||||
&& !relevantOrders.Any(r => r.OrderId == x.OrderId) // Not return in closed orders
|
||||
).ToList();
|
||||
|
||||
var additionalUpdates = new List<SharedFuturesOrder>();
|
||||
foreach (var order in openOrdersNotReturned)
|
||||
{
|
||||
additionalUpdates.Add(order with
|
||||
{
|
||||
Status = SharedOrderStatus.Canceled
|
||||
});
|
||||
}
|
||||
|
||||
relevantOrders = relevantOrders.Concat(additionalUpdates).ToArray();
|
||||
if (relevantOrders.Length > 0)
|
||||
await HandleUpdateAsync(UpdateSource.Poll, relevantOrders).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return anyError;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
{
|
||||
/// <summary>
|
||||
/// Futures user trade tracker
|
||||
/// </summary>
|
||||
public class FuturesUserTradeTracker : UserDataItemTracker<SharedUserTrade>
|
||||
{
|
||||
private readonly IFuturesOrderRestClient _restClient;
|
||||
private readonly IUserTradeSocketClient? _socketClient;
|
||||
private readonly ExchangeParameters? _exchangeParameters;
|
||||
|
||||
internal Func<string[]>? GetTrackedOrderIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public FuturesUserTradeTracker(
|
||||
ILogger logger,
|
||||
IFuturesOrderRestClient restClient,
|
||||
IUserTradeSocketClient? socketClient,
|
||||
TrackerItemConfig config,
|
||||
IEnumerable<SharedSymbol> symbols,
|
||||
bool onlyTrackProvidedSymbols,
|
||||
ExchangeParameters? exchangeParameters = null
|
||||
) : base(logger, UserDataType.Trades, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||
|
||||
_restClient = restClient;
|
||||
_socketClient = socketClient;
|
||||
_exchangeParameters = exchangeParameters;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetKey(SharedUserTrade item) => item.Id;
|
||||
/// <inheritdoc />
|
||||
protected override bool? CheckIfUpdateShouldBeApplied(SharedUserTrade existingItem, SharedUserTrade updateItem) => false;
|
||||
/// <inheritdoc />
|
||||
protected override bool Update(SharedUserTrade existingItem, SharedUserTrade updateItem) => false; // trades are never updated
|
||||
/// <inheritdoc />
|
||||
protected override TimeSpan GetAge(DateTime time, SharedUserTrade item) => time - item.Timestamp;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<bool> DoPollAsync()
|
||||
{
|
||||
var anyError = false;
|
||||
foreach (var symbol in _symbols)
|
||||
{
|
||||
var fromTimeTrades = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
||||
var updatedPollTime = DateTime.UtcNow;
|
||||
var tradesResult = await _restClient.GetFuturesUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!tradesResult.Success)
|
||||
{
|
||||
anyError = true;
|
||||
|
||||
_initialPollingError ??= tradesResult.Error;
|
||||
if (!_firstPollDone)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastDataTimeBeforeDisconnect = null;
|
||||
_lastPollTime = updatedPollTime;
|
||||
|
||||
// Filter trades to only include where timestamp is after the start time OR it's part of an order we're tracking
|
||||
var relevantTrades = tradesResult.Data.Where(x => x.Timestamp >= _startTime || (GetTrackedOrderIds?.Invoke() ?? []).Any(o => o == x.OrderId)).ToArray();
|
||||
if (relevantTrades.Length > 0)
|
||||
await HandleUpdateAsync(UpdateSource.Poll, tradesResult.Data).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return anyError;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
||||
|
||||
return ExchangeHelpers.ProcessQueuedAsync<SharedUserTrade[]>(
|
||||
async handler => await _socketClient.SubscribeToUserTradeUpdatesAsync(new SubscribeUserTradeRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
||||
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
{
|
||||
/// <summary>
|
||||
/// Position tracker
|
||||
/// </summary>
|
||||
public class PositionTracker : UserDataItemTracker<SharedPosition>
|
||||
{
|
||||
private readonly IFuturesOrderRestClient _restClient;
|
||||
private readonly IPositionSocketClient? _socketClient;
|
||||
private readonly ExchangeParameters? _exchangeParameters;
|
||||
|
||||
/// <summary>
|
||||
/// Whether websocket position updates are full snapshots and missing positions should be considered 0
|
||||
/// </summary>
|
||||
protected bool WebsocketPositionUpdatesAreFullSnapshots { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public PositionTracker(
|
||||
ILogger logger,
|
||||
IFuturesOrderRestClient restClient,
|
||||
IPositionSocketClient? socketClient,
|
||||
TrackerItemConfig config,
|
||||
IEnumerable<SharedSymbol> symbols,
|
||||
bool onlyTrackProvidedSymbols,
|
||||
bool websocketPositionUpdatesAreFullSnapshots,
|
||||
ExchangeParameters? exchangeParameters = null
|
||||
) : base(logger, UserDataType.Positions, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||
|
||||
_restClient = restClient;
|
||||
_socketClient = socketClient;
|
||||
_exchangeParameters = exchangeParameters;
|
||||
|
||||
WebsocketPositionUpdatesAreFullSnapshots = websocketPositionUpdatesAreFullSnapshots;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool Update(SharedPosition existingItem, SharedPosition updateItem)
|
||||
{
|
||||
// Some other way to way to determine sequence? Maybe timestamp?
|
||||
var changed = false;
|
||||
if (existingItem.AverageOpenPrice != updateItem.AverageOpenPrice)
|
||||
{
|
||||
existingItem.AverageOpenPrice = updateItem.AverageOpenPrice;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (existingItem.Leverage != updateItem.Leverage)
|
||||
{
|
||||
existingItem.Leverage = updateItem.Leverage;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (existingItem.LiquidationPrice != updateItem.LiquidationPrice)
|
||||
{
|
||||
existingItem.LiquidationPrice = updateItem.LiquidationPrice;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (existingItem.PositionSize != updateItem.PositionSize)
|
||||
{
|
||||
existingItem.PositionSize = updateItem.PositionSize;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (existingItem.StopLossPrice != updateItem.StopLossPrice)
|
||||
{
|
||||
existingItem.StopLossPrice = updateItem.StopLossPrice;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (existingItem.TakeProfitPrice != updateItem.TakeProfitPrice)
|
||||
{
|
||||
existingItem.TakeProfitPrice = updateItem.TakeProfitPrice;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.UnrealizedPnl != null && existingItem.UnrealizedPnl != updateItem.UnrealizedPnl)
|
||||
{
|
||||
existingItem.UnrealizedPnl = updateItem.UnrealizedPnl;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.UpdateTime != null && existingItem.UpdateTime != updateItem.UpdateTime)
|
||||
{
|
||||
existingItem.UpdateTime = updateItem.UpdateTime;
|
||||
// If update time is the only changed prop don't mark it as changed
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override async Task HandleUpdateAsync(UpdateSource source, SharedPosition[] @event)
|
||||
{
|
||||
LastUpdateTime = DateTime.UtcNow;
|
||||
|
||||
List<SharedPosition>? toRemove = null;
|
||||
foreach (var item in @event)
|
||||
{
|
||||
if (item is SharedSymbolModel symbolModel)
|
||||
{
|
||||
if (symbolModel.SharedSymbol == null)
|
||||
{
|
||||
toRemove ??= new List<SharedPosition>();
|
||||
toRemove.Add(item);
|
||||
}
|
||||
else if (_onlyTrackProvidedSymbols
|
||||
&& !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
|
||||
{
|
||||
toRemove ??= new List<SharedPosition>();
|
||||
toRemove.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove != null)
|
||||
@event = @event.Except(toRemove).ToArray();
|
||||
|
||||
if (!_onlyTrackProvidedSymbols)
|
||||
UpdateSymbolsList(@event.Where(x => x.PositionSize > 0).OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
||||
|
||||
|
||||
// Update local store
|
||||
var updatedItems = @event.Select(GetKey).ToList();
|
||||
|
||||
if (WebsocketPositionUpdatesAreFullSnapshots)
|
||||
{
|
||||
// Reset any tracking position to zero/null values when it's no longer in the snapshot as it means there is no open position any more
|
||||
var notInSnapshot = _store.Where(x => !updatedItems.Contains(x.Key) && x.Value.PositionSize != 0).ToList();
|
||||
foreach (var position in notInSnapshot)
|
||||
{
|
||||
position.Value.UpdateTime = DateTime.UtcNow;
|
||||
position.Value.AverageOpenPrice = null;
|
||||
position.Value.LiquidationPrice = null;
|
||||
position.Value.PositionSize = 0;
|
||||
position.Value.StopLossPrice = null;
|
||||
position.Value.TakeProfitPrice = null;
|
||||
position.Value.UnrealizedPnl = null;
|
||||
updatedItems.Add(position.Key);
|
||||
|
||||
LastChangeTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in @event)
|
||||
{
|
||||
bool existed = false;
|
||||
_store.AddOrUpdate(GetKey(item), item, (key, existing) =>
|
||||
{
|
||||
existed = true;
|
||||
if (CheckIfUpdateShouldBeApplied(existing, item) == false)
|
||||
{
|
||||
updatedItems.Remove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
var updated = Update(existing, item);
|
||||
if (!updated)
|
||||
{
|
||||
updatedItems.Remove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Updated {DataType} {Item}", DataType, key);
|
||||
LastChangeTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
return existing;
|
||||
});
|
||||
|
||||
if (!existed)
|
||||
{
|
||||
_logger.LogDebug("Added {DataType} {Item}", DataType, GetKey(item));
|
||||
LastChangeTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedItems.Count > 0)
|
||||
{
|
||||
await InvokeUpdate(
|
||||
new UserDataUpdate<SharedPosition[]>(source, _exchange, _store.Where(x => updatedItems.Contains(x.Key)).Select(x => x.Value).ToArray())).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetKey(SharedPosition item) =>
|
||||
item.SharedSymbol!.TradingMode + item.SharedSymbol.BaseAsset + item.SharedSymbol.QuoteAsset + item.PositionMode + (item.PositionMode != SharedPositionMode.OneWay ? item.PositionSide.ToString() : "");
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool? CheckIfUpdateShouldBeApplied(SharedPosition existingItem, SharedPosition updateItem) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
||||
|
||||
return ExchangeHelpers.ProcessQueuedAsync<SharedPosition[]>(
|
||||
async handler => await _socketClient.SubscribeToPositionUpdatesAsync(new SubscribePositionRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
||||
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<bool> DoPollAsync()
|
||||
{
|
||||
var anyError = false;
|
||||
var positionsResult = await _restClient.GetPositionsAsync(new GetPositionsRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!positionsResult.Success)
|
||||
{
|
||||
anyError = true;
|
||||
|
||||
_initialPollingError ??= positionsResult.Error;
|
||||
if (!_firstPollDone)
|
||||
return anyError;
|
||||
}
|
||||
else
|
||||
{
|
||||
await HandleUpdateAsync(UpdateSource.Poll, positionsResult.Data).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return anyError;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
{
|
||||
/// <summary>
|
||||
/// Spot order tracker
|
||||
/// </summary>
|
||||
public class SpotOrderTracker : UserDataItemTracker<SharedSpotOrder>
|
||||
{
|
||||
private readonly ISpotOrderRestClient _restClient;
|
||||
private readonly ISpotOrderSocketClient? _socketClient;
|
||||
private readonly ExchangeParameters? _exchangeParameters;
|
||||
private readonly bool _requiresSymbolParameterOpenOrders;
|
||||
|
||||
internal event Func<UpdateSource, SharedUserTrade[], Task>? OnTradeUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SpotOrderTracker(
|
||||
ILogger logger,
|
||||
ISpotOrderRestClient restClient,
|
||||
ISpotOrderSocketClient? socketClient,
|
||||
TrackerItemConfig config,
|
||||
IEnumerable<SharedSymbol> symbols,
|
||||
bool onlyTrackProvidedSymbols,
|
||||
ExchangeParameters? exchangeParameters = null
|
||||
) : base(logger, UserDataType.Orders, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||
|
||||
_restClient = restClient;
|
||||
_socketClient = socketClient;
|
||||
_exchangeParameters = exchangeParameters;
|
||||
|
||||
_requiresSymbolParameterOpenOrders = restClient.GetOpenSpotOrdersOptions.RequiredOptionalParameters.Any(x => x.Name == "Symbol");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool Update(SharedSpotOrder existingItem, SharedSpotOrder updateItem)
|
||||
{
|
||||
var changed = false;
|
||||
if (updateItem.AveragePrice != null && updateItem.AveragePrice != existingItem.AveragePrice)
|
||||
{
|
||||
existingItem.AveragePrice = updateItem.AveragePrice;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.OrderPrice != null && updateItem.OrderPrice != existingItem.OrderPrice)
|
||||
{
|
||||
existingItem.OrderPrice = updateItem.OrderPrice;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.Fee != null && updateItem.Fee != existingItem.Fee)
|
||||
{
|
||||
existingItem.Fee = updateItem.Fee;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.FeeAsset != null && updateItem.FeeAsset != existingItem.FeeAsset)
|
||||
{
|
||||
existingItem.FeeAsset = updateItem.FeeAsset;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.OrderQuantity != null && updateItem.OrderQuantity != existingItem.OrderQuantity)
|
||||
{
|
||||
existingItem.OrderQuantity ??= new SharedOrderQuantity();
|
||||
if (updateItem.OrderQuantity.QuantityInBaseAsset != null)
|
||||
{
|
||||
existingItem.OrderQuantity.QuantityInBaseAsset = updateItem.OrderQuantity.QuantityInBaseAsset;
|
||||
changed = true;
|
||||
}
|
||||
if (updateItem.OrderQuantity.QuantityInQuoteAsset != null)
|
||||
{
|
||||
existingItem.OrderQuantity.QuantityInQuoteAsset = updateItem.OrderQuantity.QuantityInQuoteAsset;
|
||||
changed = true;
|
||||
}
|
||||
if (updateItem.OrderQuantity.QuantityInContracts != null)
|
||||
{
|
||||
existingItem.OrderQuantity.QuantityInContracts = updateItem.OrderQuantity.QuantityInContracts;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateItem.QuantityFilled != null && updateItem.QuantityFilled != existingItem.QuantityFilled)
|
||||
{
|
||||
existingItem.QuantityFilled ??= new SharedOrderQuantity();
|
||||
if (updateItem.QuantityFilled.QuantityInBaseAsset != null)
|
||||
{
|
||||
existingItem.QuantityFilled.QuantityInBaseAsset = updateItem.QuantityFilled.QuantityInBaseAsset;
|
||||
changed = true;
|
||||
}
|
||||
if (updateItem.QuantityFilled.QuantityInQuoteAsset != null)
|
||||
{
|
||||
existingItem.QuantityFilled.QuantityInQuoteAsset = updateItem.QuantityFilled.QuantityInQuoteAsset;
|
||||
changed = true;
|
||||
}
|
||||
if (updateItem.QuantityFilled.QuantityInContracts != null)
|
||||
{
|
||||
existingItem.QuantityFilled.QuantityInContracts = updateItem.QuantityFilled.QuantityInContracts;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateItem.Status != existingItem.Status)
|
||||
{
|
||||
existingItem.Status = updateItem.Status;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (updateItem.UpdateTime != null && updateItem.UpdateTime != existingItem.UpdateTime)
|
||||
{
|
||||
existingItem.UpdateTime = updateItem.UpdateTime;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetKey(SharedSpotOrder item) => item.OrderId;
|
||||
/// <inheritdoc />
|
||||
protected override TimeSpan GetAge(DateTime time, SharedSpotOrder item) => item.Status == SharedOrderStatus.Open ? TimeSpan.Zero : time - (item.UpdateTime ?? item.CreateTime ?? time);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool? CheckIfUpdateShouldBeApplied(SharedSpotOrder existingItem, SharedSpotOrder updateItem)
|
||||
{
|
||||
if (existingItem.Status == SharedOrderStatus.Open && updateItem.Status != SharedOrderStatus.Open)
|
||||
// status changed from open to not open
|
||||
return true;
|
||||
|
||||
if (existingItem.Status != SharedOrderStatus.Open && updateItem.Status == SharedOrderStatus.Open)
|
||||
// status changed from not open to open; stale
|
||||
return false;
|
||||
|
||||
if (existingItem.UpdateTime != null && updateItem.UpdateTime != null)
|
||||
{
|
||||
// If both have an update time base of that
|
||||
if (existingItem.UpdateTime < updateItem.UpdateTime)
|
||||
return true;
|
||||
|
||||
if (existingItem.UpdateTime > updateItem.UpdateTime)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existingItem.QuantityFilled != null && updateItem.QuantityFilled != null)
|
||||
{
|
||||
if (existingItem.QuantityFilled.QuantityInBaseAsset != null && updateItem.QuantityFilled.QuantityInBaseAsset != null)
|
||||
{
|
||||
// If base quantity is not null we can base it on that
|
||||
if (existingItem.QuantityFilled.QuantityInBaseAsset < updateItem.QuantityFilled.QuantityInBaseAsset)
|
||||
return true;
|
||||
|
||||
else if (existingItem.QuantityFilled.QuantityInBaseAsset > updateItem.QuantityFilled.QuantityInBaseAsset)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (existingItem.QuantityFilled.QuantityInQuoteAsset != null && updateItem.QuantityFilled.QuantityInQuoteAsset != null)
|
||||
{
|
||||
// If quote quantity is not null we can base it on that
|
||||
if (existingItem.QuantityFilled.QuantityInQuoteAsset < updateItem.QuantityFilled.QuantityInQuoteAsset)
|
||||
return true;
|
||||
|
||||
else if (existingItem.QuantityFilled.QuantityInQuoteAsset > updateItem.QuantityFilled.QuantityInQuoteAsset)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingItem.Fee != null && updateItem.Fee != null)
|
||||
{
|
||||
// Higher fee means later processing
|
||||
if (existingItem.Fee < updateItem.Fee)
|
||||
return true;
|
||||
|
||||
if (existingItem.Fee > updateItem.Fee)
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected internal override async Task HandleUpdateAsync(UpdateSource source, SharedSpotOrder[] @event)
|
||||
{
|
||||
await base.HandleUpdateAsync(source, @event).ConfigureAwait(false);
|
||||
|
||||
var trades = @event.Where(x => x.LastTrade != null).Select(x => x.LastTrade!).ToArray();
|
||||
if (trades.Length != 0 && OnTradeUpdate != null)
|
||||
await OnTradeUpdate(source, trades).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
||||
|
||||
return ExchangeHelpers.ProcessQueuedAsync<SharedSpotOrder[]>(
|
||||
async handler => await _socketClient.SubscribeToSpotOrderUpdatesAsync(new SubscribeSpotOrderRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
||||
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<bool> DoPollAsync()
|
||||
{
|
||||
var anyError = false;
|
||||
List<SharedSpotOrder> openOrders = new List<SharedSpotOrder>();
|
||||
|
||||
if (!_requiresSymbolParameterOpenOrders)
|
||||
{
|
||||
var openOrdersResult = await _restClient.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!openOrdersResult.Success)
|
||||
{
|
||||
anyError = true;
|
||||
|
||||
_initialPollingError ??= openOrdersResult.Error;
|
||||
if (!_firstPollDone)
|
||||
return anyError;
|
||||
}
|
||||
else
|
||||
{
|
||||
openOrders.AddRange(openOrdersResult.Data);
|
||||
await HandleUpdateAsync(UpdateSource.Poll, openOrdersResult.Data).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var symbol in _symbols.ToList())
|
||||
{
|
||||
var openOrdersResult = await _restClient.GetOpenSpotOrdersAsync(new GetOpenOrdersRequest(symbol, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!openOrdersResult.Success)
|
||||
{
|
||||
anyError = true;
|
||||
|
||||
_initialPollingError ??= openOrdersResult.Error;
|
||||
if (!_firstPollDone)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
openOrders.AddRange(openOrdersResult.Data);
|
||||
await HandleUpdateAsync(UpdateSource.Poll, openOrdersResult.Data).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_firstPollDone && anyError)
|
||||
return anyError;
|
||||
|
||||
foreach (var symbol in _symbols.ToList())
|
||||
{
|
||||
var fromTimeOrders = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
||||
var updatedPollTime = DateTime.UtcNow;
|
||||
var closedOrdersResult = await _restClient.GetClosedSpotOrdersAsync(new GetClosedOrdersRequest(symbol, startTime: fromTimeOrders, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!closedOrdersResult.Success)
|
||||
{
|
||||
anyError = true;
|
||||
|
||||
_initialPollingError ??= closedOrdersResult.Error;
|
||||
if (!_firstPollDone)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastDataTimeBeforeDisconnect = null;
|
||||
_lastPollTime = updatedPollTime;
|
||||
|
||||
// Filter orders to only include where close time is after the start time
|
||||
var relevantOrders = closedOrdersResult.Data.Where(x =>
|
||||
x.UpdateTime != null && x.UpdateTime >= _startTime // Updated after the tracker start time
|
||||
|| x.CreateTime != null && x.CreateTime >= _startTime // Created after the tracker start time
|
||||
|| x.CreateTime == null && x.UpdateTime == null // Unknown time
|
||||
).ToArray();
|
||||
|
||||
// Check for orders which are no longer returned in either open/closed and assume they're canceled without fill
|
||||
var openOrdersNotReturned = Values.Where(x =>
|
||||
x.SharedSymbol!.BaseAsset == symbol.BaseAsset && x.SharedSymbol.QuoteAsset == symbol.QuoteAsset // Orders for the same symbol
|
||||
&& x.QuantityFilled?.IsZero == true // With no filled value
|
||||
&& !openOrders.Any(r => r.OrderId == x.OrderId) // Not returned in open orders
|
||||
&& !relevantOrders.Any(r => r.OrderId == x.OrderId) // Not return in closed orders
|
||||
).ToList();
|
||||
|
||||
var additionalUpdates = new List<SharedSpotOrder>();
|
||||
foreach (var order in openOrdersNotReturned)
|
||||
{
|
||||
additionalUpdates.Add(order with
|
||||
{
|
||||
Status = SharedOrderStatus.Canceled
|
||||
});
|
||||
}
|
||||
|
||||
relevantOrders = relevantOrders.Concat(additionalUpdates).ToArray();
|
||||
if (relevantOrders.Length > 0)
|
||||
await HandleUpdateAsync(UpdateSource.Poll, relevantOrders).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return anyError;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
{
|
||||
/// <summary>
|
||||
/// Spot user trade tracker
|
||||
/// </summary>
|
||||
public class SpotUserTradeTracker : UserDataItemTracker<SharedUserTrade>
|
||||
{
|
||||
private readonly ISpotOrderRestClient _restClient;
|
||||
private readonly IUserTradeSocketClient? _socketClient;
|
||||
private readonly ExchangeParameters? _exchangeParameters;
|
||||
|
||||
internal Func<string[]>? GetTrackedOrderIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SpotUserTradeTracker(
|
||||
ILogger logger,
|
||||
ISpotOrderRestClient restClient,
|
||||
IUserTradeSocketClient? socketClient,
|
||||
TrackerItemConfig config,
|
||||
IEnumerable<SharedSymbol> symbols,
|
||||
bool onlyTrackProvidedSymbols,
|
||||
ExchangeParameters? exchangeParameters = null
|
||||
) : base(logger, UserDataType.Trades, restClient.Exchange, config, onlyTrackProvidedSymbols, symbols)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
config = config with { PollIntervalConnected = config.PollIntervalDisconnected };
|
||||
|
||||
_restClient = restClient;
|
||||
_socketClient = socketClient;
|
||||
_exchangeParameters = exchangeParameters;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetKey(SharedUserTrade item) => item.Id;
|
||||
/// <inheritdoc />
|
||||
protected override bool? CheckIfUpdateShouldBeApplied(SharedUserTrade existingItem, SharedUserTrade updateItem) => false;
|
||||
/// <inheritdoc />
|
||||
protected override bool Update(SharedUserTrade existingItem, SharedUserTrade updateItem) => false; // Trades are never updated
|
||||
/// <inheritdoc />
|
||||
protected override TimeSpan GetAge(DateTime time, SharedUserTrade item) => time - item.Timestamp;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<bool> DoPollAsync()
|
||||
{
|
||||
var anyError = false;
|
||||
foreach (var symbol in _symbols)
|
||||
{
|
||||
var fromTimeTrades = _lastDataTimeBeforeDisconnect ?? _lastPollTime ?? _startTime;
|
||||
var updatedPollTime = DateTime.UtcNow;
|
||||
var tradesResult = await _restClient.GetSpotUserTradesAsync(new GetUserTradesRequest(symbol, startTime: fromTimeTrades, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!tradesResult.Success)
|
||||
{
|
||||
anyError = true;
|
||||
|
||||
_initialPollingError ??= tradesResult.Error;
|
||||
if (!_firstPollDone)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastDataTimeBeforeDisconnect = null;
|
||||
_lastPollTime = updatedPollTime;
|
||||
|
||||
// Filter trades to only include where timestamp is after the start time OR it's part of an order we're tracking
|
||||
var relevantTrades = tradesResult.Data.Where(x => x.Timestamp >= _startTime || (GetTrackedOrderIds?.Invoke() ?? []).Any(o => o == x.OrderId)).ToArray();
|
||||
if (relevantTrades.Length > 0)
|
||||
await HandleUpdateAsync(UpdateSource.Poll, tradesResult.Data).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return anyError;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey)
|
||||
{
|
||||
if (_socketClient == null)
|
||||
return Task.FromResult(new CallResult<UpdateSubscription?>(data: null));
|
||||
|
||||
return ExchangeHelpers.ProcessQueuedAsync<SharedUserTrade[]>(
|
||||
async handler => await _socketClient.SubscribeToUserTradeUpdatesAsync(new SubscribeUserTradeRequest(listenKey, exchangeParameters: _exchangeParameters), handler, ct: _cts!.Token).ConfigureAwait(false),
|
||||
x => HandleUpdateAsync(UpdateSource.Push, x.Data))!;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Trackers.UserData.Interfaces;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData.ItemTrackers
|
||||
{
|
||||
/// <summary>
|
||||
/// User data tracker
|
||||
/// </summary>
|
||||
public abstract class UserDataItemTracker
|
||||
{
|
||||
private bool _connected;
|
||||
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
protected ILogger _logger;
|
||||
/// <summary>
|
||||
/// Polling wait event
|
||||
/// </summary>
|
||||
protected AsyncResetEvent _pollWaitEvent = new AsyncResetEvent(false, true);
|
||||
/// <summary>
|
||||
/// Initial polling done event
|
||||
/// </summary>
|
||||
protected AsyncResetEvent _initialPollDoneEvent = new AsyncResetEvent(false, false);
|
||||
/// <summary>
|
||||
/// The error from the initial polling;
|
||||
/// </summary>
|
||||
protected Error? _initialPollingError;
|
||||
/// <summary>
|
||||
/// Polling task
|
||||
/// </summary>
|
||||
protected Task? _pollTask;
|
||||
/// <summary>
|
||||
/// Cancellation token
|
||||
/// </summary>
|
||||
protected CancellationTokenSource? _cts;
|
||||
/// <summary>
|
||||
/// Websocket subscription
|
||||
/// </summary>
|
||||
protected UpdateSubscription? _subscription;
|
||||
/// <summary>
|
||||
/// Start time
|
||||
/// </summary>
|
||||
protected DateTime? _startTime = null;
|
||||
/// <summary>
|
||||
/// Last polling attempt
|
||||
/// </summary>
|
||||
protected DateTime? _lastPollAttempt;
|
||||
/// <summary>
|
||||
/// Last polling timestamp
|
||||
/// </summary>
|
||||
protected DateTime? _lastPollTime;
|
||||
/// <summary>
|
||||
/// Timestamp of last message received before websocket disconnecting
|
||||
/// </summary>
|
||||
protected DateTime? _lastDataTimeBeforeDisconnect;
|
||||
/// <summary>
|
||||
/// Whether last polling was successful
|
||||
/// </summary>
|
||||
protected bool _lastPollSuccess;
|
||||
/// <summary>
|
||||
/// Whether first polling was done
|
||||
/// </summary>
|
||||
protected bool _firstPollDone;
|
||||
/// <summary>
|
||||
/// Whether websocket was disconnected before a polling
|
||||
/// </summary>
|
||||
protected bool _wasDisconnected;
|
||||
/// <summary>
|
||||
/// Poll at the start
|
||||
/// </summary>
|
||||
protected bool _pollAtStart;
|
||||
/// <summary>
|
||||
/// Poll interval when connected
|
||||
/// </summary>
|
||||
protected TimeSpan _pollIntervalConnected;
|
||||
/// <summary>
|
||||
/// Poll interval when disconnected
|
||||
/// </summary>
|
||||
protected TimeSpan _pollIntervalDisconnected;
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
protected string _exchange;
|
||||
/// <summary>
|
||||
/// Time completed data is retained
|
||||
/// </summary>
|
||||
public TimeSpan _retentionTime;
|
||||
|
||||
/// <summary>
|
||||
/// Data type
|
||||
/// </summary>
|
||||
public UserDataType DataType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp an update was handled. Does not necessarily mean the data was changed
|
||||
/// </summary>
|
||||
public DateTime? LastUpdateTime { get; protected set; }
|
||||
/// <summary>
|
||||
/// Timestamp any change was applied to the data
|
||||
/// </summary>
|
||||
public DateTime? LastChangeTime { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Connection status changed
|
||||
/// </summary>
|
||||
public event Action<bool>? OnConnectedChange;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public UserDataItemTracker(ILogger logger, UserDataType dataType, string exchange)
|
||||
{
|
||||
_logger = logger;
|
||||
_exchange = exchange;
|
||||
|
||||
DataType = dataType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the tracker
|
||||
/// </summary>
|
||||
/// <param name="listenKey">Optional listen key</param>
|
||||
public abstract Task<CallResult> StartAsync(string? listenKey);
|
||||
|
||||
/// <summary>
|
||||
/// Stop the tracker
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task StopAsync()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
|
||||
if (_pollTask != null)
|
||||
await _pollTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the delay until next poll
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected TimeSpan? GetNextPollDelay()
|
||||
{
|
||||
if (!_firstPollDone && _pollAtStart)
|
||||
// First polling should be done immediately
|
||||
return TimeSpan.Zero;
|
||||
|
||||
if (!Connected)
|
||||
{
|
||||
if (_pollIntervalDisconnected == TimeSpan.Zero)
|
||||
// No polling interval
|
||||
return null;
|
||||
|
||||
return _pollIntervalDisconnected;
|
||||
}
|
||||
|
||||
if (_pollIntervalConnected == TimeSpan.Zero)
|
||||
// No polling interval
|
||||
return null;
|
||||
|
||||
// Wait for next poll
|
||||
return _pollIntervalConnected;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Connected
|
||||
{
|
||||
get => _connected;
|
||||
protected set
|
||||
{
|
||||
if (_connected == value)
|
||||
return;
|
||||
|
||||
_connected = value;
|
||||
if (!_connected)
|
||||
_wasDisconnected = true;
|
||||
else
|
||||
_pollWaitEvent.Set();
|
||||
|
||||
OnConnectedChange?.Invoke(_connected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User data tracker
|
||||
/// </summary>
|
||||
public abstract class UserDataItemTracker<T> : UserDataItemTracker, IUserDataTracker<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Data store
|
||||
/// </summary>
|
||||
protected ConcurrentDictionary<string, T> _store = new ConcurrentDictionary<string, T>(StringComparer.InvariantCultureIgnoreCase);
|
||||
/// <summary>
|
||||
/// Tracked symbols list
|
||||
/// </summary>
|
||||
protected readonly List<SharedSymbol> _symbols;
|
||||
/// <summary>
|
||||
/// Symbol lock
|
||||
/// </summary>
|
||||
protected object _symbolLock = new object();
|
||||
/// <summary>
|
||||
/// Only track provided symbols setting
|
||||
/// </summary>
|
||||
protected bool _onlyTrackProvidedSymbols;
|
||||
/// <summary>
|
||||
/// Is SharedSymbol model
|
||||
/// </summary>
|
||||
protected bool _isSymbolModel;
|
||||
|
||||
/// <inheritdoc />
|
||||
public T[] Values
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_retentionTime != TimeSpan.MaxValue)
|
||||
{
|
||||
var timestamp = DateTime.UtcNow;
|
||||
foreach (var value in _store.Values)
|
||||
{
|
||||
if (GetAge(timestamp, value) > _retentionTime)
|
||||
_store.TryRemove(GetKey(value), out _);
|
||||
}
|
||||
}
|
||||
|
||||
return _store.Values.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Func<UserDataUpdate<T[]>, Task>? OnUpdate;
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<SharedSymbol> TrackedSymbols => _symbols;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public UserDataItemTracker(ILogger logger, UserDataType dataType, string exchange, TrackerItemConfig config, bool onlyTrackProvidedSymbols, IEnumerable<SharedSymbol>? symbols) : base(logger, dataType, exchange)
|
||||
{
|
||||
_onlyTrackProvidedSymbols = onlyTrackProvidedSymbols;
|
||||
_symbols = symbols?.ToList() ?? [];
|
||||
|
||||
_pollIntervalDisconnected = config.PollIntervalDisconnected;
|
||||
_pollIntervalConnected = config.PollIntervalConnected;
|
||||
_pollAtStart = config.PollAtStart;
|
||||
_retentionTime = config is TrackerTimedItemConfig timeConfig ? timeConfig.RetentionTime : TimeSpan.MaxValue;
|
||||
_isSymbolModel = typeof(T).IsSubclassOf(typeof(SharedSymbolModel));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke OnUpdate event
|
||||
/// </summary>
|
||||
protected async Task InvokeUpdate(UserDataUpdate<T[]> data)
|
||||
{
|
||||
if (OnUpdate == null)
|
||||
return;
|
||||
|
||||
await OnUpdate(data).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async override Task<CallResult> StartAsync(string? listenKey)
|
||||
{
|
||||
_startTime = DateTime.UtcNow;
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
var start = await SubscribeAsync(listenKey).ConfigureAwait(false);
|
||||
if (!start)
|
||||
return start;
|
||||
|
||||
Connected = true;
|
||||
|
||||
_pollTask = PollAsync();
|
||||
|
||||
await _initialPollDoneEvent.WaitAsync().ConfigureAwait(false);
|
||||
if (_initialPollingError != null)
|
||||
{
|
||||
await StopAsync().ConfigureAwait(false);
|
||||
return new CallResult(_initialPollingError);
|
||||
}
|
||||
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe the websocket
|
||||
/// </summary>
|
||||
public async Task<CallResult> SubscribeAsync(string? listenKey)
|
||||
{
|
||||
var subscriptionResult = await DoSubscribeAsync(listenKey).ConfigureAwait(false);
|
||||
if (!subscriptionResult)
|
||||
{
|
||||
// Failed
|
||||
// ..
|
||||
return subscriptionResult;
|
||||
}
|
||||
|
||||
if (subscriptionResult.Data == null)
|
||||
{
|
||||
// No subscription available
|
||||
// ..
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
|
||||
_subscription = subscriptionResult.Data;
|
||||
_subscription.SubscriptionStatusChanged += SubscriptionStatusChanged;
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the unique identifier for the item
|
||||
/// </summary>
|
||||
protected abstract string GetKey(T item);
|
||||
/// <summary>
|
||||
/// Check whether an update should be applied
|
||||
/// </summary>
|
||||
protected abstract bool? CheckIfUpdateShouldBeApplied(T existingItem, T updateItem);
|
||||
/// <summary>
|
||||
/// Update an existing item with an update
|
||||
/// </summary>
|
||||
protected abstract bool Update(T existingItem, T updateItem);
|
||||
/// <summary>
|
||||
/// Get the age of an item
|
||||
/// </summary>
|
||||
protected virtual TimeSpan GetAge(DateTime time, T item) => TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Update the tracked symbol list with potential new symbols
|
||||
/// </summary>
|
||||
/// <param name="symbols"></param>
|
||||
protected void UpdateSymbolsList(IEnumerable<SharedSymbol> symbols)
|
||||
{
|
||||
lock (_symbolLock)
|
||||
{
|
||||
foreach (var symbol in symbols.Distinct())
|
||||
{
|
||||
if (!_symbols.Any(x => x.TradingMode == symbol.TradingMode && x.BaseAsset == symbol.BaseAsset && x.QuoteAsset == symbol.QuoteAsset))
|
||||
{
|
||||
_symbols.Add(symbol);
|
||||
_logger.LogDebug("Adding {BaseAsset}/{QuoteAsset} to symbol tracking list", symbol.BaseAsset, symbol.QuoteAsset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle an update
|
||||
/// </summary>
|
||||
protected internal virtual async Task HandleUpdateAsync(UpdateSource source, T[] @event)
|
||||
{
|
||||
LastUpdateTime = DateTime.UtcNow;
|
||||
|
||||
if (_isSymbolModel)
|
||||
{
|
||||
List<T>? toRemove = null;
|
||||
foreach (var item in @event)
|
||||
{
|
||||
if (item is SharedSymbolModel symbolModel)
|
||||
{
|
||||
if (symbolModel.SharedSymbol == null)
|
||||
{
|
||||
toRemove ??= new List<T>();
|
||||
toRemove.Add(item);
|
||||
}
|
||||
else if (_onlyTrackProvidedSymbols
|
||||
&& !_symbols.Any(y => y.TradingMode == symbolModel.SharedSymbol!.TradingMode && y.BaseAsset == symbolModel.SharedSymbol.BaseAsset && y.QuoteAsset == symbolModel.SharedSymbol.QuoteAsset))
|
||||
{
|
||||
toRemove ??= new List<T>();
|
||||
toRemove.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove != null)
|
||||
@event = @event.Except(toRemove).ToArray();
|
||||
|
||||
if (!_onlyTrackProvidedSymbols)
|
||||
UpdateSymbolsList(@event.OfType<SharedSymbolModel>().Select(x => x.SharedSymbol!));
|
||||
}
|
||||
|
||||
// Update local store
|
||||
var updatedItems = @event.Select(GetKey).ToList();
|
||||
|
||||
foreach (var item in @event)
|
||||
{
|
||||
bool existed = false;
|
||||
_store.AddOrUpdate(GetKey(item), item, (key, existing) =>
|
||||
{
|
||||
existed = true;
|
||||
if (CheckIfUpdateShouldBeApplied(existing, item) == false)
|
||||
{
|
||||
updatedItems.Remove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
var updated = Update(existing, item);
|
||||
if (!updated)
|
||||
{
|
||||
updatedItems.Remove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogTrace("Updated {DataType} {Item}", DataType, key);
|
||||
LastChangeTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
return existing;
|
||||
});
|
||||
|
||||
if (!existed)
|
||||
{
|
||||
_logger.LogTrace("Added {DataType} {Item}", DataType, GetKey(item));
|
||||
LastChangeTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedItems.Count > 0 && OnUpdate != null)
|
||||
{
|
||||
await OnUpdate.Invoke(
|
||||
new UserDataUpdate<T[]>(source, _exchange, _store.Where(x => updatedItems.Contains(x.Key)).Select(x => x.Value).ToArray())).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Websocket subscription implementation
|
||||
/// </summary>
|
||||
protected abstract Task<CallResult<UpdateSubscription?>> DoSubscribeAsync(string? listenKey);
|
||||
|
||||
/// <summary>
|
||||
/// Polling task
|
||||
/// </summary>
|
||||
protected async Task PollAsync()
|
||||
{
|
||||
while (!_cts!.IsCancellationRequested)
|
||||
{
|
||||
var delayForNextPoll = GetNextPollDelay();
|
||||
if (delayForNextPoll != TimeSpan.Zero)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (delayForNextPoll != null)
|
||||
_logger.LogTrace("{DataType} delay for next polling: {Delay}", DataType, delayForNextPoll);
|
||||
|
||||
await _pollWaitEvent.WaitAsync(delayForNextPoll, _cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
var currentlyFirstPoll = !_firstPollDone;
|
||||
_firstPollDone = true;
|
||||
if (_cts.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
if (_lastPollAttempt != null
|
||||
&& (DateTime.UtcNow - _lastPollAttempt.Value) < TimeSpan.FromSeconds(2)
|
||||
&& !(Connected && _wasDisconnected))
|
||||
{
|
||||
if (_lastPollSuccess)
|
||||
// If last poll was less than 2 seconds ago and it was successful don't bother immediately polling again
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Connected)
|
||||
_wasDisconnected = false;
|
||||
|
||||
_lastPollSuccess = false;
|
||||
|
||||
try
|
||||
{
|
||||
var anyError = await DoPollAsync().ConfigureAwait(false);
|
||||
|
||||
_initialPollDoneEvent.Set();
|
||||
_lastPollAttempt = DateTime.UtcNow;
|
||||
_lastPollSuccess = !anyError;
|
||||
|
||||
if (anyError && currentlyFirstPoll && _pollAtStart)
|
||||
{
|
||||
if (_initialPollingError == null)
|
||||
throw new Exception("Error in initial polling but error not set");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "{DataType} UserDataTracker polling exception", DataType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polling implementation
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected abstract Task<bool> DoPollAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Handle subscription status change
|
||||
/// </summary>
|
||||
/// <param name="newState"></param>
|
||||
private void SubscriptionStatusChanged(SubscriptionStatus newState)
|
||||
{
|
||||
_logger.LogDebug("{DataType} stream status changed: {NewState}", DataType, newState);
|
||||
|
||||
if (newState == SubscriptionStatus.Pending)
|
||||
{
|
||||
// Record last data receive time since we need to request data from that timestamp on when polling
|
||||
// Only set to new value if it isn't already set since if we disconnect/reconnect a couple of times without
|
||||
// managing to do a poll we don't want to override the time since we still need to request that earlier data
|
||||
|
||||
if (_lastDataTimeBeforeDisconnect == null)
|
||||
{
|
||||
_lastDataTimeBeforeDisconnect = _subscription!.LastReceiveTime;
|
||||
|
||||
// When changing to pending (disconnected) trigger polling to start checking
|
||||
_pollWaitEvent.Set();
|
||||
}
|
||||
}
|
||||
|
||||
Connected = newState == SubscriptionStatus.Subscribed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracker configuration
|
||||
/// </summary>
|
||||
public record TrackerItemConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Interval to poll data at as backup, even when the websocket stream is still connected.
|
||||
/// </summary>
|
||||
public TimeSpan PollIntervalConnected { get; set; } = TimeSpan.Zero;
|
||||
/// <summary>
|
||||
/// Interval to poll data at while the websocket is disconnected.
|
||||
/// </summary>
|
||||
public TimeSpan PollIntervalDisconnected { get; set; } = TimeSpan.FromSeconds(30);
|
||||
/// <summary>
|
||||
/// Whether to poll for data initially when starting the tracker.
|
||||
/// </summary>
|
||||
public bool PollAtStart { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="pollAtStart">Whether to poll for data initially when starting the tracker</param>
|
||||
/// <param name="pollIntervalConnected">Interval to poll data at as backup, even when the websocket stream is still connected</param>
|
||||
/// <param name="pollIntervalDisconnected">Interval to poll data at while the websocket is disconnected</param>
|
||||
public TrackerItemConfig(bool pollAtStart, TimeSpan pollIntervalConnected, TimeSpan pollIntervalDisconnected)
|
||||
{
|
||||
PollAtStart = pollAtStart;
|
||||
PollIntervalConnected = pollIntervalConnected;
|
||||
PollIntervalDisconnected = pollIntervalDisconnected;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public record TrackerTimedItemConfig: TrackerItemConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The timespan data is retained after being completed
|
||||
/// </summary>
|
||||
public TimeSpan RetentionTime { get; set; } = TimeSpan.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="pollAtStart">Whether to poll for data initially when starting the tracker</param>
|
||||
/// <param name="pollIntervalConnected">Interval to poll data at as backup, even when the websocket stream is still connected</param>
|
||||
/// <param name="pollIntervalDisconnected">Interval to poll data at while the websocket is disconnected</param>
|
||||
/// <param name="retentionTime">The timespan data is retained after being completed</param>
|
||||
public TrackerTimedItemConfig(bool pollAtStart, TimeSpan pollIntervalConnected, TimeSpan pollIntervalDisconnected, TimeSpan retentionTime) : base(pollAtStart, pollIntervalConnected, pollIntervalDisconnected)
|
||||
{
|
||||
RetentionTime = retentionTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace CryptoExchange.Net.Trackers.UserData.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Update source
|
||||
/// </summary>
|
||||
public enum UpdateSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Polling result
|
||||
/// </summary>
|
||||
Poll,
|
||||
/// <summary>
|
||||
/// Websocket push
|
||||
/// </summary>
|
||||
Push
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// User data tracker configuration
|
||||
/// </summary>
|
||||
public abstract record UserDataTrackerConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbols to initially track, used when polling data. Other symbols will get tracked when updates are received for orders or trades on a new symbol and when there are open orders or positions on a new symbol. To only track the symbols specified here set `OnlyTrackProvidedSymbols` to true.
|
||||
/// </summary>
|
||||
public IEnumerable<SharedSymbol> TrackedSymbols { get; set; } = [];
|
||||
/// <summary>
|
||||
/// If true only orders and trades in the `Symbols` options will get tracked, data on other symbols will be ignored.
|
||||
/// </summary>
|
||||
public bool OnlyTrackProvidedSymbols { get; set; } = false;
|
||||
/// <summary>
|
||||
/// Whether to track order trades, can lead to increased requests when polling since they're requested per symbol.
|
||||
/// </summary>
|
||||
public bool TrackTrades { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spot user data tracker config
|
||||
/// </summary>
|
||||
public record SpotUserDataTrackerConfig : UserDataTrackerConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Balance tracking config
|
||||
/// </summary>
|
||||
public TrackerItemConfig BalancesConfig { get; set; } = new TrackerItemConfig(true, TimeSpan.Zero, TimeSpan.FromSeconds(10));
|
||||
/// <summary>
|
||||
/// Order tracking config
|
||||
/// </summary>
|
||||
public TrackerTimedItemConfig OrdersConfig { get; set; } = new TrackerTimedItemConfig(true, TimeSpan.Zero, TimeSpan.FromSeconds(30), TimeSpan.MaxValue);
|
||||
/// <summary>
|
||||
/// Trade tracking config
|
||||
/// </summary>
|
||||
public TrackerTimedItemConfig UserTradesConfig { get; set; } = new TrackerTimedItemConfig(false, TimeSpan.Zero, TimeSpan.FromSeconds(30), TimeSpan.MaxValue);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Futures user data tracker config
|
||||
/// </summary>
|
||||
public record FuturesUserDataTrackerConfig : UserDataTrackerConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Balance tracking config
|
||||
/// </summary>
|
||||
public TrackerItemConfig BalancesConfig { get; set; } = new TrackerItemConfig(true, TimeSpan.Zero, TimeSpan.FromSeconds(10));
|
||||
/// <summary>
|
||||
/// Order tracking config
|
||||
/// </summary>
|
||||
public TrackerTimedItemConfig OrdersConfig { get; set; } = new TrackerTimedItemConfig(true, TimeSpan.Zero, TimeSpan.FromSeconds(30), TimeSpan.MaxValue);
|
||||
/// <summary>
|
||||
/// Trade tracking config
|
||||
/// </summary>
|
||||
public TrackerTimedItemConfig UserTradesConfig { get; set; } = new TrackerTimedItemConfig(false, TimeSpan.Zero, TimeSpan.FromSeconds(30), TimeSpan.MaxValue);
|
||||
/// <summary>
|
||||
/// Position tracking config
|
||||
/// </summary>
|
||||
public TrackerItemConfig PositionConfig { get; set; } = new TrackerItemConfig(true, TimeSpan.Zero, TimeSpan.FromSeconds(30));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace CryptoExchange.Net.Trackers.UserData.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Data type
|
||||
/// </summary>
|
||||
public enum UserDataType
|
||||
{
|
||||
/// <summary>
|
||||
/// Balances
|
||||
/// </summary>
|
||||
Balances,
|
||||
/// <summary>
|
||||
/// Orders
|
||||
/// </summary>
|
||||
Orders,
|
||||
/// <summary>
|
||||
/// Trades
|
||||
/// </summary>
|
||||
Trades,
|
||||
/// <summary>
|
||||
/// Positions
|
||||
/// </summary>
|
||||
Positions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace CryptoExchange.Net.Trackers.UserData.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// User data update
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Data type</typeparam>
|
||||
public class UserDataUpdate<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Source
|
||||
/// </summary>
|
||||
public UpdateSource Source { get; set; }
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
public string Exchange { get; set; }
|
||||
/// <summary>
|
||||
/// Data
|
||||
/// </summary>
|
||||
public T Data { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public UserDataUpdate(UpdateSource source, string exchange, T data)
|
||||
{
|
||||
Source = source;
|
||||
Exchange = exchange;
|
||||
Data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData
|
||||
{
|
||||
/// <summary>
|
||||
/// User data tracker
|
||||
/// </summary>
|
||||
public abstract class UserDataTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
protected readonly ILogger _logger;
|
||||
/// <summary>
|
||||
/// Listen key to use for subscriptions
|
||||
/// </summary>
|
||||
protected string? _listenKey;
|
||||
/// <summary>
|
||||
/// List of data trackers
|
||||
/// </summary>
|
||||
protected abstract UserDataItemTracker[] DataTrackers { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? UserIdentifier { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Connected status changed
|
||||
/// </summary>
|
||||
public event Action<UserDataType, bool>? OnConnectedChange;
|
||||
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
public string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether all trackers are full connected
|
||||
/// </summary>
|
||||
public bool Connected => DataTrackers.All(x => x.Connected);
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public UserDataTracker(
|
||||
ILogger logger,
|
||||
string exchange,
|
||||
UserDataTrackerConfig config,
|
||||
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;
|
||||
|
||||
Exchange = exchange;
|
||||
UserIdentifier = userIdentifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the data tracker
|
||||
/// </summary>
|
||||
public async Task<CallResult> StartAsync()
|
||||
{
|
||||
foreach(var tracker in DataTrackers)
|
||||
tracker.OnConnectedChange += (x) => OnConnectedChange?.Invoke(tracker.DataType, x);
|
||||
|
||||
var result = await DoStartAsync().ConfigureAwait(false);
|
||||
if (!result)
|
||||
return result;
|
||||
|
||||
var tasks = new List<Task<CallResult>>();
|
||||
foreach (var dataTracker in DataTrackers)
|
||||
tasks.Add(dataTracker.StartAsync(_listenKey));
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
if (!tasks.All(x => x.Result.Success))
|
||||
{
|
||||
await Task.WhenAll(DataTrackers.Select(x => x.StopAsync())).ConfigureAwait(false);
|
||||
return tasks.First(x => !x.Result.Success).Result;
|
||||
}
|
||||
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation specific start logic
|
||||
/// </summary>
|
||||
protected abstract Task<CallResult> DoStartAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Stop the data tracker
|
||||
/// </summary>
|
||||
public async Task StopAsync()
|
||||
{
|
||||
_logger.LogDebug("Stopping UserDataTracker");
|
||||
var tasks = new List<Task>();
|
||||
foreach (var dataTracker in DataTrackers)
|
||||
tasks.Add(dataTracker.StopAsync());
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
_logger.LogDebug("Stopped UserDataTracker");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
|
||||
using CryptoExchange.Net.Trackers.UserData.Interfaces;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData
|
||||
{
|
||||
/// <summary>
|
||||
/// User futures data tracker
|
||||
/// </summary>
|
||||
public abstract class UserFuturesDataTracker : UserDataTracker, IUserFuturesDataTracker
|
||||
{
|
||||
private readonly IFuturesSymbolRestClient _symbolClient;
|
||||
private readonly IListenKeyRestClient? _listenKeyClient;
|
||||
private readonly ExchangeParameters? _exchangeParameters;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override UserDataItemTracker[] DataTrackers { get; }
|
||||
/// <summary>
|
||||
/// Balances tracker
|
||||
/// </summary>
|
||||
public IUserDataTracker<SharedBalance> Balances { get; }
|
||||
/// <summary>
|
||||
/// Orders tracker
|
||||
/// </summary>
|
||||
public IUserDataTracker<SharedFuturesOrder> Orders { get; }
|
||||
/// <summary>
|
||||
/// Positions tracker
|
||||
/// </summary>
|
||||
public IUserDataTracker<SharedPosition> Positions { get; }
|
||||
/// <summary>
|
||||
/// Trades tracker
|
||||
/// </summary>
|
||||
public IUserDataTracker<SharedUserTrade>? Trades { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether websocket position updates are full snapshots and missing positions should be considered 0
|
||||
/// </summary>
|
||||
protected abstract bool WebsocketPositionUpdatesAreFullSnapshots { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public UserFuturesDataTracker(
|
||||
ILogger logger,
|
||||
IFuturesSymbolRestClient symbolRestClient,
|
||||
IListenKeyRestClient? listenKeyRestClient,
|
||||
IBalanceRestClient balanceRestClient,
|
||||
IBalanceSocketClient? balanceSocketClient,
|
||||
IFuturesOrderRestClient futuresOrderRestClient,
|
||||
IFuturesOrderSocketClient? futuresOrderSocketClient,
|
||||
IUserTradeSocketClient? userTradeSocketClient,
|
||||
IPositionSocketClient? positionSocketClient,
|
||||
string? userIdentifier,
|
||||
FuturesUserDataTrackerConfig config,
|
||||
SharedAccountType? accountType = null,
|
||||
ExchangeParameters? exchangeParameters = null) : base(logger, symbolRestClient.Exchange, config, userIdentifier)
|
||||
{
|
||||
// create trackers
|
||||
_symbolClient = symbolRestClient;
|
||||
_listenKeyClient = listenKeyRestClient;
|
||||
_exchangeParameters = exchangeParameters;
|
||||
|
||||
var trackers = new List<UserDataItemTracker>();
|
||||
|
||||
var balanceAccountType = accountType ?? SharedAccountType.PerpetualLinearFutures;
|
||||
var balanceTracker = new BalanceTracker(logger, balanceRestClient, balanceSocketClient, balanceAccountType, config.BalancesConfig, exchangeParameters);
|
||||
Balances = balanceTracker;
|
||||
trackers.Add(balanceTracker);
|
||||
|
||||
var orderTracker = new FuturesOrderTracker(logger, futuresOrderRestClient, futuresOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||
Orders = orderTracker;
|
||||
trackers.Add(orderTracker);
|
||||
|
||||
var positionTracker = new PositionTracker(logger, futuresOrderRestClient, positionSocketClient, config.PositionConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, WebsocketPositionUpdatesAreFullSnapshots, exchangeParameters);
|
||||
Positions = positionTracker;
|
||||
trackers.Add(positionTracker);
|
||||
|
||||
if (config.TrackTrades)
|
||||
{
|
||||
var tradeTracker = new FuturesUserTradeTracker(logger, futuresOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||
Trades = tradeTracker;
|
||||
trackers.Add(tradeTracker);
|
||||
|
||||
orderTracker.OnTradeUpdate += tradeTracker.HandleUpdateAsync;
|
||||
tradeTracker.GetTrackedOrderIds = () => orderTracker.Values.Select(x => x.OrderId).ToArray();
|
||||
}
|
||||
|
||||
DataTrackers = trackers.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<CallResult> DoStartAsync()
|
||||
{
|
||||
var symbolResult = await _symbolClient.GetFuturesSymbolsAsync(new GetSymbolsRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!symbolResult)
|
||||
{
|
||||
_logger.LogWarning("Failed to start UserFuturesDataTracker; symbols request failed: {Error}", symbolResult.Error);
|
||||
return symbolResult;
|
||||
}
|
||||
|
||||
if (_listenKeyClient != null)
|
||||
{
|
||||
var lkResult = await _listenKeyClient.StartListenKeyAsync(new StartListenKeyRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!lkResult)
|
||||
{
|
||||
_logger.LogWarning("Failed to start UserFuturesDataTracker; listen key request failed: {Error}", lkResult.Error);
|
||||
return lkResult;
|
||||
}
|
||||
|
||||
_listenKey = lkResult.Data;
|
||||
}
|
||||
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Trackers.UserData.Interfaces;
|
||||
using CryptoExchange.Net.Trackers.UserData.Objects;
|
||||
using CryptoExchange.Net.Trackers.UserData.ItemTrackers;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.UserData
|
||||
{
|
||||
/// <summary>
|
||||
/// Spot user data tracker
|
||||
/// </summary>
|
||||
public class UserSpotDataTracker : UserDataTracker, IUserSpotDataTracker
|
||||
{
|
||||
private readonly ISpotSymbolRestClient _symbolClient;
|
||||
private readonly IListenKeyRestClient? _listenKeyClient;
|
||||
private readonly ExchangeParameters? _exchangeParameters;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override UserDataItemTracker[] DataTrackers { get; }
|
||||
/// <inheritdoc />
|
||||
public IUserDataTracker<SharedBalance> Balances { get; }
|
||||
/// <inheritdoc />
|
||||
public IUserDataTracker<SharedSpotOrder> Orders { get; }
|
||||
/// <inheritdoc />
|
||||
public IUserDataTracker<SharedUserTrade>? Trades { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public UserSpotDataTracker(
|
||||
ILogger logger,
|
||||
ISpotSymbolRestClient symbolRestClient,
|
||||
IListenKeyRestClient? listenKeyRestClient,
|
||||
IBalanceRestClient balanceRestClient,
|
||||
IBalanceSocketClient? balanceSocketClient,
|
||||
ISpotOrderRestClient spotOrderRestClient,
|
||||
ISpotOrderSocketClient? spotOrderSocketClient,
|
||||
IUserTradeSocketClient? userTradeSocketClient,
|
||||
string? userIdentifier,
|
||||
SpotUserDataTrackerConfig config,
|
||||
ExchangeParameters? exchangeParameters = null) : base(logger, symbolRestClient.Exchange, config, userIdentifier)
|
||||
{
|
||||
// create trackers
|
||||
_symbolClient = symbolRestClient;
|
||||
_listenKeyClient = listenKeyRestClient;
|
||||
_exchangeParameters = exchangeParameters;
|
||||
|
||||
var trackers = new List<UserDataItemTracker>();
|
||||
|
||||
var balanceTracker = new BalanceTracker(logger, balanceRestClient, balanceSocketClient, SharedAccountType.Spot, config.BalancesConfig, exchangeParameters);
|
||||
Balances = balanceTracker;
|
||||
trackers.Add(balanceTracker);
|
||||
|
||||
var orderTracker = new SpotOrderTracker(logger, spotOrderRestClient, spotOrderSocketClient, config.OrdersConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||
Orders = orderTracker;
|
||||
trackers.Add(orderTracker);
|
||||
|
||||
if (config.TrackTrades)
|
||||
{
|
||||
var tradeTracker = new SpotUserTradeTracker(logger, spotOrderRestClient, userTradeSocketClient, config.UserTradesConfig, config.TrackedSymbols, config.OnlyTrackProvidedSymbols, exchangeParameters);
|
||||
Trades = tradeTracker;
|
||||
trackers.Add(tradeTracker);
|
||||
|
||||
orderTracker.OnTradeUpdate += tradeTracker.HandleUpdateAsync;
|
||||
tradeTracker.GetTrackedOrderIds = () => orderTracker.Values.Select(x => x.OrderId).ToArray();
|
||||
}
|
||||
|
||||
DataTrackers = trackers.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<CallResult> DoStartAsync()
|
||||
{
|
||||
var symbolResult = await _symbolClient.GetSpotSymbolsAsync(new GetSymbolsRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!symbolResult)
|
||||
{
|
||||
_logger.LogWarning("Failed to start UserSpotDataTracker; symbols request failed: {Error}", symbolResult.Error);
|
||||
return symbolResult;
|
||||
}
|
||||
|
||||
if (_listenKeyClient != null)
|
||||
{
|
||||
var lkResult = await _listenKeyClient.StartListenKeyAsync(new StartListenKeyRequest(exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!lkResult)
|
||||
{
|
||||
_logger.LogWarning("Failed to start UserSpotDataTracker; listen key request failed: {Error}", lkResult.Error);
|
||||
return lkResult;
|
||||
}
|
||||
|
||||
_listenKey = lkResult.Data;
|
||||
}
|
||||
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,26 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||
|
||||
## Release notes
|
||||
* Version 10.4.1 - 06 Feb 2026
|
||||
* Updated UserDataTracker to only track symbol when position size > 0
|
||||
* Update UserDataTracker log verbosity
|
||||
|
||||
* Version 10.4.0 - 06 Feb 2026
|
||||
* Added user data tracking logic
|
||||
* Added LastReceiveTime, SocketStatus and SubscriptionStatus properties to UpdateSubscription
|
||||
* Added SharedTransferStatus Enum and property to SharedDeposit
|
||||
* Added PositionMode property to SharedPosition model
|
||||
* Added IsZero property to SharedQuantity
|
||||
* Added additional methods for requesting supported symbols to Shared ISpotSymbolRestClient/IFuturesSymbolRestClient interfaces
|
||||
* Added Disposed property on BaseClient and IRestClient/ISocketClient interfaces
|
||||
* Added AutoTimestamp option for socket client
|
||||
* Renamed IWebSocket LastActionTime to LastReceiveTime
|
||||
* Refactored AsyncResetEvent implementation
|
||||
* Updated CryptoExchangeWebsocketClient LastReceiveTime logic
|
||||
* Updated Subscription status change event handler to run sync instead of separate task
|
||||
* Updated Interval property access on KlineTracker to public
|
||||
* Fixed socket client timestamp offset bug
|
||||
|
||||
* Version 10.3.1 - 27 Jan 2026
|
||||
* Fixed potential collection modified exception upon logging message not handled in websocket message handling
|
||||
|
||||
|
||||
Reference in New Issue
Block a user