mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-07 07:56:12 +00:00
Trackers (#218)
Fix for intermittently failing rate limiting test Added ConnectionId to RequestDefinition to correctly handle connection and path rate limiting configuration Added ValidateMessage method to websocket Query object to filter messages even though it is matched to the query based on the ListenIdentifier Added KlineTracker and TradeTracker implementation
This commit is contained in:
parent
ed007b5272
commit
9e86a08327
@ -302,7 +302,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
||||
|
||||
|
@ -0,0 +1,292 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
public static class TrackerLoggingExtensions
|
||||
{
|
||||
private static readonly Action<ILogger, string, SyncStatus, SyncStatus, Exception?> _klineTrackerStatusChanged;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerStarting;
|
||||
private static readonly Action<ILogger, string, string, Exception?> _klineTrackerStartFailed;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerStarted;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerStopping;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerStopped;
|
||||
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerInitialDataSet;
|
||||
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerKlineUpdated;
|
||||
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerKlineAdded;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionLost;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionClosed;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionRestored;
|
||||
|
||||
private static readonly Action<ILogger, string, SyncStatus, SyncStatus, Exception?> _tradeTrackerStatusChanged;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStarting;
|
||||
private static readonly Action<ILogger, string, string, Exception?> _tradeTrackerStartFailed;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStarted;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStopping;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStopped;
|
||||
private static readonly Action<ILogger, string, int, long, Exception?> _tradeTrackerInitialDataSet;
|
||||
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerPreSnapshotSkip;
|
||||
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerPreSnapshotApplied;
|
||||
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerTradeAdded;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionLost;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionClosed;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionRestored;
|
||||
|
||||
static TrackerLoggingExtensions()
|
||||
{
|
||||
_klineTrackerStatusChanged = LoggerMessage.Define<string, SyncStatus, SyncStatus>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6001, "KlineTrackerStatusChanged"),
|
||||
"Kline tracker for {Symbol} status changed: {OldStatus} => {NewStatus}");
|
||||
|
||||
_klineTrackerStarting = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6002, "KlineTrackerStarting"),
|
||||
"Kline tracker for {Symbol} starting");
|
||||
|
||||
_klineTrackerStartFailed = LoggerMessage.Define<string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6003, "KlineTrackerStartFailed"),
|
||||
"Kline tracker for {Symbol} failed to start: {Error}");
|
||||
|
||||
_klineTrackerStarted = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(6004, "KlineTrackerStarted"),
|
||||
"Kline tracker for {Symbol} started");
|
||||
|
||||
_klineTrackerStopping = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6005, "KlineTrackerStopping"),
|
||||
"Kline tracker for {Symbol} stopping");
|
||||
|
||||
_klineTrackerStopped = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(6006, "KlineTrackerStopped"),
|
||||
"Kline tracker for {Symbol} stopped");
|
||||
|
||||
_klineTrackerInitialDataSet = LoggerMessage.Define<string, DateTime>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6007, "KlineTrackerInitialDataSet"),
|
||||
"Kline tracker for {Symbol} initial data set, last timestamp: {LastTime}");
|
||||
|
||||
_klineTrackerKlineUpdated = LoggerMessage.Define<string, DateTime>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6008, "KlineTrackerKlineUpdated"),
|
||||
"Kline tracker for {Symbol} kline updated for open time: {LastTime}");
|
||||
|
||||
_klineTrackerKlineAdded = LoggerMessage.Define<string, DateTime>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6009, "KlineTrackerKlineAdded"),
|
||||
"Kline tracker for {Symbol} new kline for open time: {LastTime}");
|
||||
|
||||
_klineTrackerConnectionLost = LoggerMessage.Define<string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6010, "KlineTrackerConnectionLost"),
|
||||
"Kline tracker for {Symbol} connection lost");
|
||||
|
||||
_klineTrackerConnectionClosed = LoggerMessage.Define<string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6011, "KlineTrackerConnectionClosed"),
|
||||
"Kline tracker for {Symbol} disconnected");
|
||||
|
||||
_klineTrackerConnectionRestored = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(6012, "KlineTrackerConnectionRestored"),
|
||||
"Kline tracker for {Symbol} successfully resynchronized");
|
||||
|
||||
|
||||
_tradeTrackerStatusChanged = LoggerMessage.Define<string, SyncStatus, SyncStatus>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6013, "KlineTrackerStatusChanged"),
|
||||
"Trade tracker for {Symbol} status changed: {OldStatus} => {NewStatus}");
|
||||
|
||||
_tradeTrackerStarting = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6014, "KlineTrackerStarting"),
|
||||
"Trade tracker for {Symbol} starting");
|
||||
|
||||
_tradeTrackerStartFailed = LoggerMessage.Define<string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6015, "KlineTrackerStartFailed"),
|
||||
"Trade tracker for {Symbol} failed to start: {Error}");
|
||||
|
||||
_tradeTrackerStarted = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(6016, "KlineTrackerStarted"),
|
||||
"Trade tracker for {Symbol} started");
|
||||
|
||||
_tradeTrackerStopping = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6017, "KlineTrackerStopping"),
|
||||
"Trade tracker for {Symbol} stopping");
|
||||
|
||||
_tradeTrackerStopped = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(6018, "KlineTrackerStopped"),
|
||||
"Trade tracker for {Symbol} stopped");
|
||||
|
||||
_tradeTrackerInitialDataSet = LoggerMessage.Define<string, int, long>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6019, "TradeTrackerInitialDataSet"),
|
||||
"Trade tracker for {Symbol} snapshot set, Count: {Count}, Last id: {LastId}");
|
||||
|
||||
_tradeTrackerPreSnapshotSkip = LoggerMessage.Define<string, long>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6020, "TradeTrackerPreSnapshotSkip"),
|
||||
"Trade tracker for {Symbol} skipping {Id}, already in snapshot");
|
||||
|
||||
_tradeTrackerPreSnapshotApplied = LoggerMessage.Define<string, long>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6021, "TradeTrackerPreSnapshotApplied"),
|
||||
"Trade tracker for {Symbol} adding {Id} from pre-snapshot");
|
||||
|
||||
_tradeTrackerTradeAdded = LoggerMessage.Define<string, long>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6022, "TradeTrackerTradeAdded"),
|
||||
"Trade tracker for {Symbol} adding trade {Id}");
|
||||
|
||||
_tradeTrackerConnectionLost = LoggerMessage.Define<string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6023, "TradeTrackerConnectionLost"),
|
||||
"Trade tracker for {Symbol} connection lost");
|
||||
|
||||
_tradeTrackerConnectionClosed = LoggerMessage.Define<string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6024, "TradeTrackerConnectionClosed"),
|
||||
"Trade tracker for {Symbol} disconnected");
|
||||
|
||||
_tradeTrackerConnectionRestored = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(6025, "TradeTrackerConnectionRestored"),
|
||||
"Trade tracker for {Symbol} successfully resynchronized");
|
||||
}
|
||||
|
||||
public static void KlineTrackerStatusChanged(this ILogger logger, string symbol, SyncStatus oldStatus, SyncStatus newStatus)
|
||||
{
|
||||
_klineTrackerStatusChanged(logger, symbol, oldStatus, newStatus, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerStarting(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerStarting(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error)
|
||||
{
|
||||
_klineTrackerStartFailed(logger, symbol, error, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerStarted(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerStarted(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerStopping(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerStopping(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerStopped(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerStopped(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerInitialDataSet(this ILogger logger, string symbol, DateTime lastTime)
|
||||
{
|
||||
_klineTrackerInitialDataSet(logger, symbol, lastTime, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerKlineUpdated(this ILogger logger, string symbol, DateTime lastTime)
|
||||
{
|
||||
_klineTrackerKlineUpdated(logger, symbol, lastTime, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerKlineAdded(this ILogger logger, string symbol, DateTime lastTime)
|
||||
{
|
||||
_klineTrackerKlineAdded(logger, symbol, lastTime, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerConnectionLost(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerConnectionLost(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerConnectionClosed(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerConnectionClosed(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerConnectionRestored(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerConnectionRestored(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStatusChanged(this ILogger logger, string symbol, SyncStatus oldStatus, SyncStatus newStatus)
|
||||
{
|
||||
_tradeTrackerStatusChanged(logger, symbol, oldStatus, newStatus, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStarting(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerStarting(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error)
|
||||
{
|
||||
_tradeTrackerStartFailed(logger, symbol, error, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStarted(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerStarted(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStopping(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerStopping(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStopped(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerStopped(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerInitialDataSet(this ILogger logger, string symbol, int count, long lastId)
|
||||
{
|
||||
_tradeTrackerInitialDataSet(logger, symbol, count, lastId, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerPreSnapshotSkip(this ILogger logger, string symbol, long lastId)
|
||||
{
|
||||
_tradeTrackerPreSnapshotSkip(logger, symbol, lastId, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerPreSnapshotApplied(this ILogger logger, string symbol, long lastId)
|
||||
{
|
||||
_tradeTrackerPreSnapshotApplied(logger, symbol, lastId, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerTradeAdded(this ILogger logger, string symbol, long lastId)
|
||||
{
|
||||
_tradeTrackerTradeAdded(logger, symbol, lastId, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerConnectionLost(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerConnectionLost(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerConnectionClosed(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerConnectionClosed(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerConnectionRestored(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerConnectionRestored(logger, symbol, null);
|
||||
}
|
||||
}
|
||||
}
|
@ -68,6 +68,33 @@
|
||||
Json
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracker sync status
|
||||
/// </summary>
|
||||
public enum SyncStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Not connected
|
||||
/// </summary>
|
||||
Disconnected,
|
||||
/// <summary>
|
||||
/// Syncing, data connection is being made
|
||||
/// </summary>
|
||||
Syncing,
|
||||
/// <summary>
|
||||
/// The connection is active, but the full data backlog is not yet reached. For example, a tracker set to retain 10 minutes of data only has 8 minutes of data at this moment.
|
||||
/// </summary>
|
||||
PartiallySynced,
|
||||
/// <summary>
|
||||
/// Synced
|
||||
/// </summary>
|
||||
Synced,
|
||||
/// <summary>
|
||||
/// Disposed
|
||||
/// </summary>
|
||||
Diposed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status of the order book
|
||||
/// </summary>
|
||||
|
@ -58,12 +58,16 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public IRateLimitGuard? LimitGuard { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether this request should never be cached
|
||||
/// </summary>
|
||||
public bool PreventCaching { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Connection id
|
||||
/// </summary>
|
||||
public int? ConnectionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
@ -18,6 +18,10 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
|
||||
/// <summary>
|
||||
/// Apply guard per connection
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString());
|
||||
/// <summary>
|
||||
/// Apply guard per API key
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key!);
|
||||
|
@ -209,7 +209,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
if (Parameters.RateLimiter != null)
|
||||
{
|
||||
var definition = new RequestDefinition(Id.ToString(), HttpMethod.Get);
|
||||
var definition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id };
|
||||
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, definition, _baseAddress, null, 1, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
return new CallResult(new ClientRateLimitError("Connection limit reached"));
|
||||
@ -475,7 +475,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
private async Task SendLoopAsync()
|
||||
{
|
||||
var requestDefinition = new RequestDefinition(Id.ToString(), HttpMethod.Get);
|
||||
var requestDefinition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id };
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
|
@ -177,6 +177,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <inheritdoc />
|
||||
public override async Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message)
|
||||
{
|
||||
var typedMessage = message.As((TServerResponse)message.Data);
|
||||
if (!ValidateMessage(typedMessage))
|
||||
return new CallResult(null);
|
||||
|
||||
CurrentResponses++;
|
||||
if (CurrentResponses == RequiredResponses)
|
||||
{
|
||||
@ -186,7 +190,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
if (Result?.Success != false)
|
||||
// If an error result is already set don't override that
|
||||
Result = HandleMessage(connection, message.As((TServerResponse)message.Data));
|
||||
Result = HandleMessage(connection, typedMessage);
|
||||
|
||||
if (CurrentResponses == RequiredResponses)
|
||||
{
|
||||
@ -198,6 +202,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
return Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate if a message is actually processable by this query
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
public virtual bool ValidateMessage(DataEvent<TServerResponse> message) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Handle the query response
|
||||
/// </summary>
|
||||
|
34
CryptoExchange.Net/Trackers/CompareValue.cs
Normal file
34
CryptoExchange.Net/Trackers/CompareValue.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Compare value
|
||||
/// </summary>
|
||||
public record CompareValue
|
||||
{
|
||||
/// <summary>
|
||||
/// The value difference
|
||||
/// </summary>
|
||||
public decimal? Difference { get; set; }
|
||||
/// <summary>
|
||||
/// The value difference percentage
|
||||
/// </summary>
|
||||
public decimal? PercentageDifference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CompareValue(decimal? value1, decimal? value2)
|
||||
{
|
||||
if (value1 == null || value2 == null)
|
||||
return;
|
||||
|
||||
Difference = value2 - value1;
|
||||
PercentageDifference = value1.Value == 0 ? null : Math.Round(value2.Value / value1.Value * 100 - 100, 4);
|
||||
}
|
||||
}
|
||||
}
|
105
CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs
Normal file
105
CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Klines
|
||||
{
|
||||
/// <summary>
|
||||
/// A tracker for kline data of a symbol
|
||||
/// </summary>
|
||||
public interface IKlineTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// The total number of klines
|
||||
/// </summary>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol name
|
||||
/// </summary>
|
||||
string SymbolName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol
|
||||
/// </summary>
|
||||
SharedSymbol Symbol { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The max number of klines tracked
|
||||
/// </summary>
|
||||
int? Limit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The max age of the data tracked
|
||||
/// </summary>
|
||||
TimeSpan? Period { get; }
|
||||
|
||||
/// <summary>
|
||||
/// From which timestamp the trades are registered
|
||||
/// </summary>
|
||||
DateTime? SyncedFrom { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sync status
|
||||
/// </summary>
|
||||
SyncStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the last kline
|
||||
/// </summary>
|
||||
SharedKline? Last { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event for when a new kline is added
|
||||
/// </summary>
|
||||
event Func<SharedKline, Task>? OnAdded;
|
||||
/// <summary>
|
||||
/// Event for when a kline is removed because it's no longer within the period/limit window
|
||||
/// </summary>
|
||||
event Func<SharedKline, Task>? OnRemoved;
|
||||
/// <summary>
|
||||
/// Event for when a kline is updated
|
||||
/// </summary>
|
||||
event Func<SharedKline, Task> OnUpdated;
|
||||
/// <summary>
|
||||
/// Event for when the sync status changes
|
||||
/// </summary>
|
||||
event Func<SyncStatus, SyncStatus, Task>? OnStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Start synchronization
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<CallResult> StartAsync(bool startWithSnapshot = true);
|
||||
|
||||
/// <summary>
|
||||
/// Stop synchronization
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task StopAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get the data tracked
|
||||
/// </summary>
|
||||
/// <param name="fromTimestamp">Start timestamp to get the data from, defaults to tracked data start time</param>
|
||||
/// <param name="toTimestamp">End timestamp to get the data until, defaults to current time</param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<SharedKline> GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get statitistics on the klines
|
||||
/// </summary>
|
||||
/// <param name="fromTimestamp">Start timestamp to get the data from, defaults to tracked data start time</param>
|
||||
/// <param name="toTimestamp">End timestamp to get the data until, defaults to current time</param>
|
||||
/// <returns></returns>
|
||||
KlinesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null);
|
||||
|
||||
}
|
||||
}
|
481
CryptoExchange.Net/Trackers/Klines/KlineTracker.cs
Normal file
481
CryptoExchange.Net/Trackers/Klines/KlineTracker.cs
Normal file
@ -0,0 +1,481 @@
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Klines
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class KlineTracker : IKlineTracker
|
||||
{
|
||||
private readonly IKlineSocketClient _socketClient;
|
||||
private readonly IKlineRestClient _restClient;
|
||||
private SyncStatus _status;
|
||||
private bool _startWithSnapshot;
|
||||
|
||||
/// <summary>
|
||||
/// The internal data structure
|
||||
/// </summary>
|
||||
protected readonly Dictionary<DateTime, SharedKline> _data = new Dictionary<DateTime, SharedKline>();
|
||||
/// <summary>
|
||||
/// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set
|
||||
/// </summary>
|
||||
protected readonly List<SharedKline> _preSnapshotQueue = new List<SharedKline>();
|
||||
/// <summary>
|
||||
/// Lock for accessing _data
|
||||
/// </summary>
|
||||
protected readonly object _lock = new object();
|
||||
/// <summary>
|
||||
/// The last time the window was applied
|
||||
/// </summary>
|
||||
protected DateTime _lastWindowApplied = DateTime.MinValue;
|
||||
/// <summary>
|
||||
/// Whether or not the data has changed since last window was applied
|
||||
/// </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;
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
protected readonly ILogger _logger;
|
||||
/// <summary>
|
||||
/// Update subscription
|
||||
/// </summary>
|
||||
protected UpdateSubscription? _updateSubscription;
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp of the first item
|
||||
/// </summary>
|
||||
protected DateTime? _firstTimestamp;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SyncStatus Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
if (value == _status)
|
||||
return;
|
||||
|
||||
var old = _status;
|
||||
_status = value;
|
||||
_logger.KlineTrackerStatusChanged(SymbolName, old, value);
|
||||
OnStatusChanged?.Invoke(old, _status);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Exchange { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SymbolName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public SharedSymbol Symbol { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int? Limit { get; }
|
||||
/// <inheritdoc/>
|
||||
public TimeSpan? Period { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime? SyncedFrom
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Period == null)
|
||||
return _firstTimestamp;
|
||||
|
||||
var max = DateTime.UtcNow - Period.Value;
|
||||
if (_firstTimestamp > max)
|
||||
return _firstTimestamp;
|
||||
|
||||
return max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ApplyWindow(true);
|
||||
return _data.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SharedKline? Last
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ApplyWindow(true);
|
||||
return _data.LastOrDefault().Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Func<SharedKline, Task>? OnAdded;
|
||||
/// <inheritdoc />
|
||||
public event Func<SharedKline, Task>? OnUpdated;
|
||||
/// <inheritdoc />
|
||||
public event Func<SharedKline, Task>? OnRemoved;
|
||||
/// <inheritdoc />
|
||||
public event Func<SyncStatus, SyncStatus, Task>? OnStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public KlineTracker(
|
||||
ILogger? logger,
|
||||
IKlineRestClient restClient,
|
||||
IKlineSocketClient socketClient,
|
||||
SharedSymbol symbol,
|
||||
SharedKlineInterval interval,
|
||||
int? limit = null,
|
||||
TimeSpan? period = null)
|
||||
{
|
||||
_logger = logger ?? new NullLogger<KlineTracker>();
|
||||
Symbol = symbol;
|
||||
SymbolName = socketClient.FormatSymbol(symbol.BaseAsset, symbol.QuoteAsset, symbol.TradingMode, symbol.DeliverTime);
|
||||
Exchange = restClient.Exchange;
|
||||
Limit = limit;
|
||||
Period = period;
|
||||
_interval = interval;
|
||||
_socketClient = socketClient;
|
||||
_restClient = restClient;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult> StartAsync(bool startWithSnapshot = true)
|
||||
{
|
||||
if (Status != SyncStatus.Disconnected)
|
||||
throw new InvalidOperationException($"Can't start syncing unless state is {SyncStatus.Disconnected}. Current state: {Status}");
|
||||
|
||||
_startWithSnapshot = startWithSnapshot;
|
||||
Status = SyncStatus.Syncing;
|
||||
_logger.KlineTrackerStarting(SymbolName);
|
||||
|
||||
var startResult = await DoStartAsync().ConfigureAwait(false);
|
||||
if (!startResult)
|
||||
{
|
||||
_logger.KlineTrackerStartFailed(SymbolName, startResult.Error!.ToString());
|
||||
Status = SyncStatus.Disconnected;
|
||||
return new CallResult(startResult.Error!);
|
||||
}
|
||||
|
||||
_updateSubscription = startResult.Data;
|
||||
_updateSubscription.ConnectionLost += HandleConnectionLost;
|
||||
_updateSubscription.ConnectionClosed += HandleConnectionClosed;
|
||||
_updateSubscription.ConnectionRestored += HandleConnectionRestored;
|
||||
Status = SyncStatus.Synced;
|
||||
_logger.KlineTrackerStarted(SymbolName);
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync()
|
||||
{
|
||||
_logger.KlineTrackerStopping(SymbolName);
|
||||
Status = SyncStatus.Disconnected;
|
||||
await DoStopAsync().ConfigureAwait(false);
|
||||
_data.Clear();
|
||||
_preSnapshotQueue.Clear();
|
||||
_logger.KlineTrackerStopped(SymbolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The start procedure needed for kline syncing, generally subscribing to an update stream and requesting the snapshot
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<UpdateSubscription>> DoStartAsync()
|
||||
{
|
||||
var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, _interval),
|
||||
update =>
|
||||
{
|
||||
AddOrUpdate(update.Data);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (!subResult)
|
||||
{
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult;
|
||||
}
|
||||
|
||||
if (!_startWithSnapshot)
|
||||
return subResult;
|
||||
|
||||
var startTime = Period == null ? (DateTime?)null : DateTime.UtcNow.Add(-Period.Value);
|
||||
if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime)
|
||||
startTime = DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value);
|
||||
|
||||
var limit = Math.Min(_restClient.GetKlinesOptions.MaxRequestDataPoints ?? _restClient.GetKlinesOptions.MaxTotalDataPoints ?? 100, Limit ?? 100);
|
||||
|
||||
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))
|
||||
{
|
||||
if (!result)
|
||||
{
|
||||
_ = subResult.Data.CloseAsync();
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult.AsError<UpdateSubscription>(result.Error!);
|
||||
}
|
||||
|
||||
if (Limit != null && data.Count > Limit)
|
||||
break;
|
||||
|
||||
data.AddRange(result.Data);
|
||||
}
|
||||
|
||||
SetInitialData(data);
|
||||
return subResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The stop procedure needed, generally stopping the update stream
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual Task DoStopAsync() => _updateSubscription?.CloseAsync() ?? Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public KlinesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null)
|
||||
{
|
||||
var compareTime = SyncedFrom?.AddSeconds(-2);
|
||||
var stats = GetStats(GetData(fromTimestamp, toTimestamp));
|
||||
stats.Complete = (fromTimestamp == null || fromTimestamp >= compareTime) && (toTimestamp == null || toTimestamp >= compareTime);
|
||||
return stats;
|
||||
}
|
||||
|
||||
private KlinesStats GetStats(IEnumerable<SharedKline> klines)
|
||||
{
|
||||
if (!klines.Any())
|
||||
return new KlinesStats();
|
||||
|
||||
return new KlinesStats
|
||||
{
|
||||
KlineCount = klines.Count(),
|
||||
FirstOpenTime = klines.First().OpenTime,
|
||||
LastOpenTime = klines.Last().OpenTime,
|
||||
HighPrice = klines.Select(d => d.LowPrice).Max(),
|
||||
LowPrice = klines.Select(d => d.HighPrice).Min(),
|
||||
Volume = klines.Select(d => d.Volume).Sum(),
|
||||
AverageVolume = Math.Round(klines.OrderByDescending(d => d.OpenTime).Skip(1).Select(d => d.Volume).DefaultIfEmpty().Average(), 8)
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<SharedKline> GetData(DateTime? since = null, DateTime? until = null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ApplyWindow(true);
|
||||
|
||||
IEnumerable<SharedKline> result = _data.Values;
|
||||
if (since != null)
|
||||
result = result.Where(d => d.OpenTime >= since);
|
||||
if (until != null)
|
||||
result = result.Where(d => d.OpenTime <= until);
|
||||
|
||||
return result.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the initial kline data snapshot
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
protected void SetInitialData(IEnumerable<SharedKline> data)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_data.Clear();
|
||||
|
||||
IEnumerable<SharedKline> items = data.OrderByDescending(d => d.OpenTime);
|
||||
if (Limit != null)
|
||||
items = items.Take(Limit.Value);
|
||||
if (Period != null)
|
||||
items = items.Where(e => e.OpenTime >= DateTime.UtcNow.Add(-Period.Value));
|
||||
|
||||
foreach (var item in items.OrderBy(d => d.OpenTime))
|
||||
_data.Add(item.OpenTime, item);
|
||||
|
||||
_snapshotSet = true;
|
||||
|
||||
foreach (var item in _preSnapshotQueue)
|
||||
{
|
||||
if (_data.ContainsKey(item.OpenTime))
|
||||
continue;
|
||||
|
||||
_data.Add(item.OpenTime, item);
|
||||
}
|
||||
|
||||
_firstTimestamp = _data.Min(v => v.Key);
|
||||
ApplyWindow(false);
|
||||
_logger.KlineTrackerInitialDataSet(SymbolName, _data.Last().Key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add or update a kline
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
protected void AddOrUpdate(SharedKline item) => AddOrUpdate(new[] { item });
|
||||
|
||||
/// <summary>
|
||||
/// Add or update klines
|
||||
/// </summary>
|
||||
/// <param name="items"></param>
|
||||
protected void AddOrUpdate(IEnumerable<SharedKline> items)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_restClient != null && _startWithSnapshot && !_snapshotSet)
|
||||
{
|
||||
_preSnapshotQueue.AddRange(items);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (_data.TryGetValue(item.OpenTime, out var existing))
|
||||
{
|
||||
_data.Remove(item.OpenTime);
|
||||
_data.Add(item.OpenTime, item);
|
||||
OnUpdated?.Invoke(item);
|
||||
_logger.KlineTrackerKlineUpdated(SymbolName, _data.Last().Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
_data.Add(item.OpenTime, item);
|
||||
OnAdded?.Invoke(item);
|
||||
_logger.KlineTrackerKlineAdded(SymbolName, _data.Last().Key);
|
||||
}
|
||||
}
|
||||
|
||||
_firstTimestamp = _data.Min(x => x.Key);
|
||||
_changed = true;
|
||||
|
||||
SetSyncStatus();
|
||||
ApplyWindow(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyWindow(bool broadcastEvents)
|
||||
{
|
||||
if (!_changed && (DateTime.UtcNow - _lastWindowApplied) < TimeSpan.FromSeconds(1))
|
||||
return;
|
||||
|
||||
if (Period != null)
|
||||
{
|
||||
var compareDate = DateTime.UtcNow.Add(-Period.Value);
|
||||
for (var i = 0; i < _data.Count; i++)
|
||||
{
|
||||
var item = _data.ElementAt(0);
|
||||
if (item.Key >= compareDate)
|
||||
break;
|
||||
|
||||
_data.Remove(item.Key);
|
||||
if (broadcastEvents)
|
||||
OnRemoved?.Invoke(item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (Limit != null && _data.Count > Limit.Value)
|
||||
{
|
||||
var toRemove = Math.Max(0, _data.Count - Limit.Value);
|
||||
for (var i = 0; i < toRemove; i++)
|
||||
{
|
||||
var item = _data.ElementAt(0);
|
||||
_data.Remove(item.Key);
|
||||
if (broadcastEvents)
|
||||
OnRemoved?.Invoke(item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
_lastWindowApplied = DateTime.UtcNow;
|
||||
_changed = false;
|
||||
}
|
||||
|
||||
private void HandleConnectionLost()
|
||||
{
|
||||
_logger.KlineTrackerConnectionLost(SymbolName);
|
||||
if (Status != SyncStatus.Disconnected)
|
||||
{
|
||||
Status = SyncStatus.Syncing;
|
||||
_snapshotSet = false;
|
||||
_firstTimestamp = null;
|
||||
_preSnapshotQueue.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectionClosed()
|
||||
{
|
||||
_logger.KlineTrackerConnectionClosed(SymbolName);
|
||||
Status = SyncStatus.Disconnected;
|
||||
_ = StopAsync();
|
||||
}
|
||||
|
||||
private async void HandleConnectionRestored(TimeSpan _)
|
||||
{
|
||||
Status = SyncStatus.Syncing;
|
||||
var success = false;
|
||||
while (!success)
|
||||
{
|
||||
if (Status != SyncStatus.Syncing)
|
||||
return;
|
||||
|
||||
var resyncResult = await DoStartAsync().ConfigureAwait(false);
|
||||
success = resyncResult;
|
||||
}
|
||||
|
||||
_logger.KlineTrackerConnectionRestored(SymbolName);
|
||||
SetSyncStatus();
|
||||
}
|
||||
|
||||
private void SetSyncStatus()
|
||||
{
|
||||
if (Status == SyncStatus.Synced)
|
||||
return;
|
||||
|
||||
if (Period != null)
|
||||
{
|
||||
if (_firstTimestamp <= DateTime.UtcNow - Period.Value)
|
||||
Status = SyncStatus.Synced;
|
||||
else
|
||||
Status = SyncStatus.PartiallySynced;
|
||||
}
|
||||
|
||||
if (Limit != null)
|
||||
{
|
||||
if (_data.Count == Limit.Value)
|
||||
Status = SyncStatus.Synced;
|
||||
else
|
||||
Status = SyncStatus.PartiallySynced;
|
||||
}
|
||||
|
||||
if (Period == null && Limit == null)
|
||||
Status = SyncStatus.Synced;
|
||||
}
|
||||
}
|
||||
}
|
30
CryptoExchange.Net/Trackers/Klines/KlinesCompare.cs
Normal file
30
CryptoExchange.Net/Trackers/Klines/KlinesCompare.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Klines
|
||||
{
|
||||
/// <summary>
|
||||
/// Klines statistics comparison
|
||||
/// </summary>
|
||||
public record KlinesCompare
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of trades
|
||||
/// </summary>
|
||||
public CompareValue? LowPriceDif { get; set; }
|
||||
/// <summary>
|
||||
/// Number of trades
|
||||
/// </summary>
|
||||
public CompareValue? HighPriceDif { get; set; }
|
||||
/// <summary>
|
||||
/// Number of trades
|
||||
/// </summary>
|
||||
public CompareValue? VolumeDif { get; set; }
|
||||
/// <summary>
|
||||
/// Number of trades
|
||||
/// </summary>
|
||||
public CompareValue? AverageVolumeDif { get; set; }
|
||||
|
||||
}
|
||||
}
|
59
CryptoExchange.Net/Trackers/Klines/KlinesStats.cs
Normal file
59
CryptoExchange.Net/Trackers/Klines/KlinesStats.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Klines
|
||||
{
|
||||
/// <summary>
|
||||
/// Klines statistics
|
||||
/// </summary>
|
||||
public record KlinesStats
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of klines
|
||||
/// </summary>
|
||||
public int KlineCount { get; set; }
|
||||
/// <summary>
|
||||
/// The kline open time of the first entry
|
||||
/// </summary>
|
||||
public DateTime? FirstOpenTime { get; set; }
|
||||
/// <summary>
|
||||
/// The kline open time of the last entry
|
||||
/// </summary>
|
||||
public DateTime? LastOpenTime { get; set; }
|
||||
/// <summary>
|
||||
/// Lowest trade price
|
||||
/// </summary>
|
||||
public decimal? LowPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Highest trade price
|
||||
/// </summary>
|
||||
public decimal? HighPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Trade volume
|
||||
/// </summary>
|
||||
public decimal Volume { get; set; }
|
||||
/// <summary>
|
||||
/// Average volume per kline
|
||||
/// </summary>
|
||||
public decimal? AverageVolume { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the data is complete
|
||||
/// </summary>
|
||||
public bool Complete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compare 2 stat snapshots to eachother
|
||||
/// </summary>
|
||||
public KlinesCompare CompareTo(KlinesStats otherStats)
|
||||
{
|
||||
return new KlinesCompare
|
||||
{
|
||||
LowPriceDif = new CompareValue(LowPrice, otherStats.LowPrice),
|
||||
HighPriceDif = new CompareValue(HighPrice, otherStats.HighPrice),
|
||||
VolumeDif = new CompareValue(Volume, otherStats.Volume),
|
||||
AverageVolumeDif = new CompareValue(AverageVolume, otherStats.AverageVolume),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
100
CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs
Normal file
100
CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Trades
|
||||
{
|
||||
/// <summary>
|
||||
/// A tracker for trades on a symbol
|
||||
/// </summary>
|
||||
public interface ITradeTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// The total number of trades
|
||||
/// </summary>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol name
|
||||
/// </summary>
|
||||
string SymbolName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol
|
||||
/// </summary>
|
||||
SharedSymbol Symbol { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The max number of trades tracked
|
||||
/// </summary>
|
||||
int? Limit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The max age of the data tracked
|
||||
/// </summary>
|
||||
TimeSpan? Period { get; }
|
||||
|
||||
/// <summary>
|
||||
/// From which timestamp the trades are registered
|
||||
/// </summary>
|
||||
DateTime? SyncedFrom { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current synchronization status
|
||||
/// </summary>
|
||||
SyncStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the last trade
|
||||
/// </summary>
|
||||
SharedTrade? Last { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event for when a new trade is added
|
||||
/// </summary>
|
||||
event Func<SharedTrade, Task>? OnAdded;
|
||||
/// <summary>
|
||||
/// Event for when a trade is removed because it's no longer within the period/limit window
|
||||
/// </summary>
|
||||
event Func<SharedTrade, Task>? OnRemoved;
|
||||
/// <summary>
|
||||
/// Event for when the sync status changes
|
||||
/// </summary>
|
||||
event Func<SyncStatus, SyncStatus, Task>? OnStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Start synchronization
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<CallResult> StartAsync(bool startWithSnapshot = true);
|
||||
|
||||
/// <summary>
|
||||
/// Stop synchronization
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task StopAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get the data tracked
|
||||
/// </summary>
|
||||
/// <param name="fromTimestamp">Start timestamp to get the data from, defaults to tracked data start time</param>
|
||||
/// <param name="toTimestamp">End timestamp to get the data until, defaults to current time</param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<SharedTrade> GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get statitistics on the trades
|
||||
/// </summary>
|
||||
/// <param name="fromTimestamp">Start timestamp to get the data from, defaults to tracked data start time</param>
|
||||
/// <param name="toTimestamp">End timestamp to get the data until, defaults to current time</param>
|
||||
/// <returns></returns>
|
||||
TradesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null);
|
||||
}
|
||||
}
|
495
CryptoExchange.Net/Trackers/Trades/TradeTracker.cs
Normal file
495
CryptoExchange.Net/Trackers/Trades/TradeTracker.cs
Normal file
@ -0,0 +1,495 @@
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Trades
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class TradeTracker : ITradeTracker
|
||||
{
|
||||
private readonly ITradeSocketClient _socketClient;
|
||||
private readonly IRecentTradeRestClient? _recentRestClient;
|
||||
private readonly ITradeHistoryRestClient? _historyRestClient;
|
||||
private SyncStatus _status;
|
||||
private long _snapshotId;
|
||||
private bool _startWithSnapshot;
|
||||
|
||||
/// <summary>
|
||||
/// The internal data structure
|
||||
/// </summary>
|
||||
protected readonly List<SharedTrade> _data = new List<SharedTrade>();
|
||||
/// <summary>
|
||||
/// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set
|
||||
/// </summary>
|
||||
protected readonly List<SharedTrade> _preSnapshotQueue = new List<SharedTrade>();
|
||||
|
||||
/// <summary>
|
||||
/// The last time the window was applied
|
||||
/// </summary>
|
||||
protected DateTime _lastWindowApplied = DateTime.MinValue;
|
||||
/// <summary>
|
||||
/// Whether or not the data has changed since last window was applied
|
||||
/// </summary>
|
||||
protected bool _changed = false;
|
||||
/// <summary>
|
||||
/// Lock for accessing _data
|
||||
/// </summary>
|
||||
protected readonly object _lock = new object();
|
||||
/// <summary>
|
||||
/// Whether the snapshot has been set
|
||||
/// </summary>
|
||||
protected bool _snapshotSet;
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
protected readonly ILogger _logger;
|
||||
/// <summary>
|
||||
/// Update subscription
|
||||
/// </summary>
|
||||
protected UpdateSubscription? _updateSubscription;
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp of the first item
|
||||
/// </summary>
|
||||
protected DateTime? _firstTimestamp;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Exchange { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SymbolName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public SharedSymbol Symbol { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int? Limit { get; }
|
||||
/// <inheritdoc/>
|
||||
public TimeSpan? Period { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SyncStatus Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
if (value == _status)
|
||||
return;
|
||||
|
||||
var old = _status;
|
||||
_status = value;
|
||||
_logger.TradeTrackerStatusChanged(SymbolName, old, value);
|
||||
OnStatusChanged?.Invoke(old, _status);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ApplyWindow(true);
|
||||
return _data.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime? SyncedFrom
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Period == null)
|
||||
return _firstTimestamp;
|
||||
|
||||
var max = DateTime.UtcNow - Period.Value;
|
||||
if (_firstTimestamp > max)
|
||||
return _firstTimestamp;
|
||||
|
||||
return max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SharedTrade? Last
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ApplyWindow(true);
|
||||
return _data.LastOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Func<SharedTrade, Task>? OnAdded;
|
||||
/// <inheritdoc />
|
||||
public event Func<SharedTrade, Task>? OnRemoved;
|
||||
/// <inheritdoc />
|
||||
public event Func<SyncStatus, SyncStatus, Task>? OnStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TradeTracker(
|
||||
ILogger? logger,
|
||||
IRecentTradeRestClient? recentRestClient,
|
||||
ITradeHistoryRestClient? historyRestClient,
|
||||
ITradeSocketClient socketClient,
|
||||
SharedSymbol symbol,
|
||||
int? limit = null,
|
||||
TimeSpan? period = null)
|
||||
{
|
||||
_logger = logger ?? new NullLogger<TradeTracker>();
|
||||
_recentRestClient = recentRestClient;
|
||||
_historyRestClient = historyRestClient;
|
||||
_socketClient = socketClient;
|
||||
Exchange = socketClient.Exchange;
|
||||
Symbol = symbol;
|
||||
SymbolName = socketClient.FormatSymbol(symbol.BaseAsset, symbol.QuoteAsset, symbol.TradingMode, symbol.DeliverTime);
|
||||
Limit = limit;
|
||||
Period = period;
|
||||
}
|
||||
|
||||
private TradesStats GetStats(IEnumerable<SharedTrade> trades)
|
||||
{
|
||||
if (!trades.Any())
|
||||
return new TradesStats();
|
||||
|
||||
return new TradesStats
|
||||
{
|
||||
TradeCount = trades.Count(),
|
||||
FirstTradeTime = trades.First().Timestamp,
|
||||
LastTradeTime = trades.Last().Timestamp,
|
||||
AveragePrice = Math.Round(trades.Select(d => d.Price).DefaultIfEmpty().Average(), 8),
|
||||
VolumeWeightedAveragePrice = trades.Any() ? Math.Round(trades.Select(d => d.Price * d.Quantity).DefaultIfEmpty().Sum() / trades.Select(d => d.Quantity).DefaultIfEmpty().Sum(), 8) : null,
|
||||
Volume = Math.Round(trades.Sum(d => d.Quantity), 8),
|
||||
QuoteVolume = Math.Round(trades.Sum(d => d.Quantity * d.Price), 8),
|
||||
BuySellRatio = Math.Round(trades.Where(x => x.Side == SharedOrderSide.Buy).Sum(x => x.Quantity) / trades.Sum(x => x.Quantity), 8)
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TradesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null)
|
||||
{
|
||||
var compareTime = SyncedFrom?.AddSeconds(-2);
|
||||
var stats = GetStats(GetData(fromTimestamp, toTimestamp));
|
||||
stats.Complete = (fromTimestamp == null || fromTimestamp >= compareTime) && (toTimestamp == null || toTimestamp >= compareTime);
|
||||
return stats;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult> StartAsync(bool startWithSnapshot = true)
|
||||
{
|
||||
if (Status != SyncStatus.Disconnected)
|
||||
throw new InvalidOperationException($"Can't start syncing unless state is {SyncStatus.Disconnected}. Current state: {Status}");
|
||||
|
||||
_startWithSnapshot = startWithSnapshot;
|
||||
Status = SyncStatus.Syncing;
|
||||
_logger.TradeTrackerStarting(SymbolName);
|
||||
var subResult = await DoStartAsync().ConfigureAwait(false);
|
||||
if (!subResult)
|
||||
{
|
||||
_logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.ToString());
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult;
|
||||
}
|
||||
|
||||
_updateSubscription = subResult.Data;
|
||||
_updateSubscription.ConnectionLost += HandleConnectionLost;
|
||||
_updateSubscription.ConnectionClosed += HandleConnectionClosed;
|
||||
_updateSubscription.ConnectionRestored += HandleConnectionRestored;
|
||||
SetSyncStatus();
|
||||
_logger.TradeTrackerStarted(SymbolName);
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync()
|
||||
{
|
||||
_logger.TradeTrackerStopping(SymbolName);
|
||||
Status = SyncStatus.Disconnected;
|
||||
await DoStopAsync().ConfigureAwait(false);
|
||||
_data.Clear();
|
||||
_preSnapshotQueue.Clear();
|
||||
_logger.TradeTrackerStopped(SymbolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The start procedure needed for trade syncing, generally subscribing to an update stream and requesting the snapshot
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<UpdateSubscription>> DoStartAsync()
|
||||
{
|
||||
var subResult = await _socketClient.SubscribeToTradeUpdatesAsync(new SubscribeTradeRequest(Symbol),
|
||||
update =>
|
||||
{
|
||||
AddData(update.Data);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (!subResult)
|
||||
{
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult;
|
||||
}
|
||||
|
||||
if (!_startWithSnapshot)
|
||||
return subResult;
|
||||
|
||||
if (_historyRestClient != null)
|
||||
{
|
||||
var startTime = Period == null ? DateTime.UtcNow.AddMinutes(-5) : DateTime.UtcNow.Add(-Period.Value);
|
||||
var request = new GetTradeHistoryRequest(Symbol, startTime, DateTime.UtcNow);
|
||||
var data = new List<SharedTrade>();
|
||||
await foreach(var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false))
|
||||
{
|
||||
if (!result)
|
||||
{
|
||||
_ = subResult.Data.CloseAsync();
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult.AsError<UpdateSubscription>(result.Error!);
|
||||
}
|
||||
|
||||
if (Limit != null && data.Count > Limit)
|
||||
break;
|
||||
|
||||
data.AddRange(result.Data);
|
||||
}
|
||||
|
||||
SetInitialData(data);
|
||||
}
|
||||
else if (_recentRestClient != null)
|
||||
{
|
||||
int? limit = null;
|
||||
if (Limit.HasValue)
|
||||
limit = Math.Min(_recentRestClient.GetRecentTradesOptions.MaxLimit, Limit.Value);
|
||||
|
||||
var snapshot = await _recentRestClient.GetRecentTradesAsync(new GetRecentTradesRequest(Symbol, limit)).ConfigureAwait(false);
|
||||
if (!snapshot)
|
||||
{
|
||||
_ = subResult.Data.CloseAsync();
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult.AsError<UpdateSubscription>(snapshot.Error!);
|
||||
}
|
||||
|
||||
SetInitialData(snapshot.Data);
|
||||
}
|
||||
|
||||
return subResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The stop procedure needed, generally stopping the update stream
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual Task DoStopAsync() => _updateSubscription?.CloseAsync() ?? Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<SharedTrade> GetData(DateTime? since = null, DateTime? until = null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ApplyWindow(true);
|
||||
|
||||
IEnumerable<SharedTrade> result = _data;
|
||||
if (since != null)
|
||||
result = result.Where(d => d.Timestamp >= since);
|
||||
if (until != null)
|
||||
result = result.Where(d => d.Timestamp <= until);
|
||||
|
||||
return result.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the initial trade data snapshot
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
protected void SetInitialData(IEnumerable<SharedTrade> data)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_data.Clear();
|
||||
|
||||
IEnumerable<SharedTrade> items = data.OrderByDescending(d => d.Timestamp);
|
||||
if (Limit != null)
|
||||
items = items.Take(Limit.Value);
|
||||
if (Period != null)
|
||||
items = items.Where(e => e.Timestamp >= DateTime.UtcNow.Add(-Period.Value));
|
||||
|
||||
_snapshotId = data.Max(d => d.Timestamp.Ticks);
|
||||
foreach (var item in items.OrderBy(d => d.Timestamp))
|
||||
_data.Add(item);
|
||||
|
||||
_snapshotSet = true;
|
||||
_changed = true;
|
||||
|
||||
_logger.TradeTrackerInitialDataSet(SymbolName, _data.Count, _snapshotId);
|
||||
|
||||
foreach (var item in _preSnapshotQueue)
|
||||
{
|
||||
if (_snapshotId >= item.Timestamp.Ticks)
|
||||
{
|
||||
_logger.TradeTrackerPreSnapshotSkip(SymbolName, item.Timestamp.Ticks);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.TradeTrackerPreSnapshotApplied(SymbolName, item.Timestamp.Ticks);
|
||||
_data.Add(item);
|
||||
}
|
||||
|
||||
_firstTimestamp = _data.Min(v => v.Timestamp);
|
||||
|
||||
ApplyWindow(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a trade
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
protected void AddData(SharedTrade item) => AddData(new[] { item });
|
||||
|
||||
/// <summary>
|
||||
/// Add a list of trades
|
||||
/// </summary>
|
||||
/// <param name="items"></param>
|
||||
protected void AddData(IEnumerable<SharedTrade> items)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if ((_recentRestClient != null || _historyRestClient != null) && _startWithSnapshot && !_snapshotSet)
|
||||
{
|
||||
_preSnapshotQueue.AddRange(items);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
_logger.TradeTrackerTradeAdded(SymbolName, item.Timestamp.Ticks);
|
||||
_data.Add(item);
|
||||
OnAdded?.Invoke(item);
|
||||
}
|
||||
|
||||
_firstTimestamp = _data.Min(x => x.Timestamp);
|
||||
_changed = true;
|
||||
SetSyncStatus();
|
||||
ApplyWindow(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyWindow(bool broadcastEvents)
|
||||
{
|
||||
if (!_changed && (DateTime.UtcNow - _lastWindowApplied) < TimeSpan.FromSeconds(1))
|
||||
return;
|
||||
|
||||
if (Period != null)
|
||||
{
|
||||
var compareDate = DateTime.UtcNow.Add(-Period.Value);
|
||||
for(var i = 0; i < _data.Count; i++)
|
||||
{
|
||||
var item = _data[0];
|
||||
if (item.Timestamp >= compareDate)
|
||||
break;
|
||||
|
||||
_data.Remove(item);
|
||||
if (broadcastEvents)
|
||||
OnRemoved?.Invoke(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (Limit != null && _data.Count > Limit.Value)
|
||||
{
|
||||
var toRemove = _data.Count - Limit.Value;
|
||||
for (var i = 0; i < toRemove; i++)
|
||||
{
|
||||
var item = _data[0];
|
||||
_data.Remove(item);
|
||||
if (broadcastEvents)
|
||||
OnRemoved?.Invoke(item);
|
||||
}
|
||||
}
|
||||
|
||||
_lastWindowApplied = DateTime.UtcNow;
|
||||
_changed = false;
|
||||
|
||||
if (Status == SyncStatus.PartiallySynced)
|
||||
// Need to check if sync status should be changed even if there may not be any new data
|
||||
SetSyncStatus();
|
||||
}
|
||||
|
||||
|
||||
private void HandleConnectionLost()
|
||||
{
|
||||
_logger.TradeTrackerConnectionLost(SymbolName);
|
||||
if (Status != SyncStatus.Disconnected)
|
||||
{
|
||||
Status = SyncStatus.Syncing;
|
||||
_snapshotSet = false;
|
||||
_firstTimestamp = null;
|
||||
_preSnapshotQueue.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectionClosed()
|
||||
{
|
||||
_logger.TradeTrackerConnectionClosed(SymbolName);
|
||||
Status = SyncStatus.Disconnected;
|
||||
_ = StopAsync();
|
||||
}
|
||||
|
||||
private async void HandleConnectionRestored(TimeSpan _)
|
||||
{
|
||||
Status = SyncStatus.Syncing;
|
||||
var success = false;
|
||||
while (!success)
|
||||
{
|
||||
if (Status != SyncStatus.Syncing)
|
||||
return;
|
||||
|
||||
var resyncResult = await DoStartAsync().ConfigureAwait(false);
|
||||
success = resyncResult;
|
||||
}
|
||||
|
||||
_logger.TradeTrackerConnectionRestored(SymbolName);
|
||||
SetSyncStatus();
|
||||
}
|
||||
|
||||
private void SetSyncStatus()
|
||||
{
|
||||
if (Status == SyncStatus.Synced)
|
||||
return;
|
||||
|
||||
if (Period != null)
|
||||
{
|
||||
if (_firstTimestamp <= DateTime.UtcNow - Period.Value)
|
||||
Status = SyncStatus.Synced;
|
||||
else
|
||||
Status = SyncStatus.PartiallySynced;
|
||||
}
|
||||
|
||||
if (Limit != null)
|
||||
{
|
||||
if (_data.Count == Limit.Value)
|
||||
Status = SyncStatus.Synced;
|
||||
else
|
||||
Status = SyncStatus.PartiallySynced;
|
||||
}
|
||||
|
||||
if (Period == null && Limit == null)
|
||||
Status = SyncStatus.Synced;
|
||||
}
|
||||
}
|
||||
}
|
37
CryptoExchange.Net/Trackers/Trades/TradesCompare.cs
Normal file
37
CryptoExchange.Net/Trackers/Trades/TradesCompare.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Trades
|
||||
{
|
||||
/// <summary>
|
||||
/// Trades statistics comparison
|
||||
/// </summary>
|
||||
public record TradesCompare
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of trades
|
||||
/// </summary>
|
||||
public CompareValue TradeCountDif { get; set; } = new CompareValue(null, null);
|
||||
/// <summary>
|
||||
/// Average trade price
|
||||
/// </summary>
|
||||
public CompareValue? AveragePriceDif { get; set; }
|
||||
/// <summary>
|
||||
/// Volume weighted average trade price
|
||||
/// </summary>
|
||||
public CompareValue? VolumeWeightedAveragePriceDif { get; set; }
|
||||
/// <summary>
|
||||
/// Volume of the trades
|
||||
/// </summary>
|
||||
public CompareValue VolumeDif { get; set; } = new CompareValue(null, null);
|
||||
/// <summary>
|
||||
/// Volume of the trades in quote asset
|
||||
/// </summary>
|
||||
public CompareValue QuoteVolumeDif { get; set; } = new CompareValue(null, null);
|
||||
/// <summary>
|
||||
/// The volume weighted Buy/Sell ratio. A 0.7 ratio means 70% of the trade volume was a buy.
|
||||
/// </summary>
|
||||
public CompareValue? BuySellRatioDif { get; set; }
|
||||
}
|
||||
}
|
65
CryptoExchange.Net/Trackers/Trades/TradesStats.cs
Normal file
65
CryptoExchange.Net/Trackers/Trades/TradesStats.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Trades
|
||||
{
|
||||
/// <summary>
|
||||
/// Trades statistics
|
||||
/// </summary>
|
||||
public record TradesStats
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of trades
|
||||
/// </summary>
|
||||
public int TradeCount { get; set; }
|
||||
/// <summary>
|
||||
/// Timestamp of the last trade
|
||||
/// </summary>
|
||||
public DateTime? FirstTradeTime { get; set; }
|
||||
/// <summary>
|
||||
/// Timestamp of the first trade
|
||||
/// </summary>
|
||||
public DateTime? LastTradeTime { get; set; }
|
||||
/// <summary>
|
||||
/// Average trade price
|
||||
/// </summary>
|
||||
public decimal? AveragePrice { get; set; }
|
||||
/// <summary>
|
||||
/// Volume weighted average trade price
|
||||
/// </summary>
|
||||
public decimal? VolumeWeightedAveragePrice { get; set; }
|
||||
/// <summary>
|
||||
/// Volume of the trades
|
||||
/// </summary>
|
||||
public decimal Volume { get; set; }
|
||||
/// <summary>
|
||||
/// Volume of the trades in quote asset
|
||||
/// </summary>
|
||||
public decimal QuoteVolume { get; set; }
|
||||
/// <summary>
|
||||
/// The volume weighted Buy/Sell ratio. A 0.7 ratio means 70% of the trade volume was a buy.
|
||||
/// </summary>
|
||||
public decimal? BuySellRatio { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the data is complete
|
||||
/// </summary>
|
||||
public bool Complete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compare 2 stat snapshots to eachother
|
||||
/// </summary>
|
||||
public TradesCompare CompareTo(TradesStats otherStats)
|
||||
{
|
||||
return new TradesCompare
|
||||
{
|
||||
TradeCountDif = new CompareValue(TradeCount, otherStats.TradeCount),
|
||||
AveragePriceDif = new CompareValue(AveragePrice, otherStats.AveragePrice),
|
||||
VolumeWeightedAveragePriceDif = new CompareValue(VolumeWeightedAveragePrice, otherStats.VolumeWeightedAveragePrice),
|
||||
VolumeDif = new CompareValue(Volume, otherStats.Volume),
|
||||
QuoteVolumeDif = new CompareValue(QuoteVolume, otherStats.QuoteVolume),
|
||||
BuySellRatioDif = new CompareValue(BuySellRatio, otherStats.BuySellRatio),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
637
docs/index.html
637
docs/index.html
@ -106,6 +106,7 @@
|
||||
<li class="nav-item"><a class="nav-link" href="#idocs_features">Additional Features</a>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="#idocs_orderbooks">Orderbooks</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#idocs_trackers">Trackers</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#idocs_logging">Logging</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#idocs_ratelimiting">Ratelimiting</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#idocs_caching">Caching</a></li>
|
||||
@ -164,6 +165,9 @@
|
||||
</table>
|
||||
<p>Note that there are 3rd party implementations going around, but only the listed ones here are created and supported by me.</p>
|
||||
<p>When using multiple of these API's the <a href="https://github.com/jkorf/CryptoClients.Net">CryptoClients.Net</a> package can be used which combines these packages and allows easy access to all exchange API's.</p>
|
||||
<table class="table table-bordered">
|
||||
<tr><td>CryptoClients</td><td><a href="https://github.com/JKorf/CryptoClients.Net">JKorf/CryptoClients.Net</a></td><td><a href="https://www.nuget.org/packages/CryptoClients.Net" target="_blank"><img src="https://img.shields.io/nuget/v/CryptoClients.net.svg?style=flat-square" /></a></a></td></tr>
|
||||
</table>
|
||||
|
||||
<h4>Supported Frameworks</h4>
|
||||
<p>
|
||||
@ -3255,6 +3259,639 @@ foreach (var book in books.Where(b => b.Status == OrderBookStatus.Synced))
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<section id="idocs_trackers">
|
||||
<h2>Trackers</h2>
|
||||
<p>
|
||||
Trackers offer a way to keep track of live data. This data can than be aggregated into statistics and different time slices can be compared to get realtime insights.
|
||||
</p>
|
||||
<p>
|
||||
Currently there are 2 different trackers available, the <code>TradeTracker</code> and the <code>KlineTracker</code>.
|
||||
|
||||
</p>
|
||||
<p>
|
||||
<b>Creation and starting</b><br />
|
||||
The example uses the <code>TradeTracker</code>, but the same logic can be applied to the <code>KlineTracker</code>.
|
||||
</p>
|
||||
<div class="tab-wrap">
|
||||
<ul class="nav nav-tabs" id="tracker" role="tablist" style="margin-bottom: -16px;">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" id="tracker-cryptoclients-tab" data-toggle="tab" href="#tracker-cryptoclients" role="tab" aria-controls="tracker-cryptoclients" aria-selected="true">CryptoClients</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-binance-tab" data-toggle="tab" href="#tracker-binance" role="tab" aria-controls="tracker-binance" aria-selected="false">Binance</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-bingx-tab" data-toggle="tab" href="#tracker-bingx" role="tab" aria-controls="tracker-bingx" aria-selected="false">BingX</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-bitfinex-tab" data-toggle="tab" href="#tracker-bitfinex" role="tab" aria-controls="tracker-bitfinex" aria-selected="false">Bitfinex</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-bitget-tab" data-toggle="tab" href="#tracker-bitget" role="tab" aria-controls="tracker-bitget" aria-selected="false">Bitget</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-bitmart-tab" data-toggle="tab" href="#tracker-bitmart" role="tab" aria-controls="tracker-bitmart" aria-selected="false">BitMart</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-bybit-tab" data-toggle="tab" href="#tracker-bybit" role="tab" aria-controls="tracker-bybit" aria-selected="false">Bybit</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-coinbase-tab" data-toggle="tab" href="#tracker-coinbase" role="tab" aria-controls="tracker-coinbase" aria-selected="false">Coinbase</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-coinex-tab" data-toggle="tab" href="#tracker-coinex" role="tab" aria-controls="tracker-coinex" aria-selected="false">Coinex</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-cryptocom-tab" data-toggle="tab" href="#tracker-cryptocom" role="tab" aria-controls="tracker-cryptocom" aria-selected="false">Crypto.com</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-gateio-tab" data-toggle="tab" href="#tracker-gateio" role="tab" aria-controls="tracker-gateio" aria-selected="false">GateIo</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-htx-tab" data-toggle="tab" href="#tracker-htx" role="tab" aria-controls="tracker-htx" aria-selected="false">HTX</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-kraken-tab" data-toggle="tab" href="#tracker-kraken" role="tab" aria-controls="tracker-kraken" aria-selected="false">Kraken</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-kucoin-tab" data-toggle="tab" href="#tracker-kucoin" role="tab" aria-controls="tracker-kucoin" aria-selected="false">Kucoin</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-mexc-tab" data-toggle="tab" href="#tracker-mexc" role="tab" aria-controls="tracker-mexc" aria-selected="false">Mexc</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tracker-okx-tab" data-toggle="tab" href="#tracker-okx" role="tab" aria-controls="tracker-okx" aria-selected="false">OKX</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content my-3" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="tracker-cryptoclients" role="tabpanel" aria-labelledby="tracker-cryptoclients-tab">
|
||||
<pre><code>var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Assuming IExchangeTrackerFactory is injected as trackerFactory
|
||||
// Create tracker dynamically by exchange name
|
||||
var tracker = trackerFactory.CreateTradeTracker("Binance", symbol);
|
||||
// OR by directly referencing the specific exchange
|
||||
tracker = trackerFactory.Binance.CreateTradeTracker(symbol);
|
||||
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-binance" role="tabpanel" aria-labelledby="tracker-binance-tab">
|
||||
<pre><code>// Either create a new factory or inject the IBinanceTrackerFactory interface
|
||||
var factory = new BinanceTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-bingx" role="tabpanel" aria-labelledby="tracker-bingx-tab">
|
||||
<pre><code>// Either create a new factory or inject the IBingXTrackerFactory interface
|
||||
var factory = new BingXTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-bitfinex" role="tabpanel" aria-labelledby="tracker-bitfinex-tab">
|
||||
<pre><code>// Either create a new factory or inject the IBitfinexTrackerFactory interface
|
||||
var factory = new BitfinexTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-bitget" role="tabpanel" aria-labelledby="tracker-bitget-tab">
|
||||
<pre><code>// Either create a new factory or inject the IBitgetTrackerFactory interface
|
||||
var factory = new BitgetTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-bitmart" role="tabpanel" aria-labelledby="tracker-bitmart-tab">
|
||||
<pre><code>// Either create a new factory or inject the IBitMartTrackerFactory interface
|
||||
var factory = new BitMartTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-bybit" role="tabpanel" aria-labelledby="tracker-bybit-tab">
|
||||
<pre><code>// Either create a new factory or inject the IBybitTrackerFactory interface
|
||||
var factory = new BybitTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-coinbase" role="tabpanel" aria-labelledby="tracker-coinbase-tab">
|
||||
<pre><code>// Either create a new factory or inject the ICoinbaseTrackerFactory interface
|
||||
var factory = new CoinbaseTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-coinex" role="tabpanel" aria-labelledby="tracker-coinex-tab">
|
||||
<pre><code>// Either create a new factory or inject the ICoinExTrackerFactory interface
|
||||
var factory = new CoinExTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-gateio" role="tabpanel" aria-labelledby="tracker-gateio-tab">
|
||||
<pre><code>// Either create a new factory or inject the IGateIoTrackerFactory interface
|
||||
var factory = new GateIoTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-cryptocom" role="tabpanel" aria-labelledby="tracker-cryptocom-tab">
|
||||
<pre><code>// Either create a new factory or inject the ICryptoComTrackerFactory interface
|
||||
var factory = new CryptoComTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-htx" role="tabpanel" aria-labelledby="tracker-htx-tab">
|
||||
<pre><code>// Either create a new factory or inject the IHTXTrackerFactory interface
|
||||
var factory = new HTXTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-kraken" role="tabpanel" aria-labelledby="tracker-kraken-tab">
|
||||
<pre><code>// Either create a new factory or inject the IKrakenTrackerFactory interface
|
||||
var factory = new KrakenTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USD");
|
||||
|
||||
// Create a tracker for ETH/USD keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-kucoin" role="tabpanel" aria-labelledby="tracker-kucoin-tab">
|
||||
<pre><code>// Either create a new factory or inject the IKucoinTrackerFactory interface
|
||||
var factory = new KucoinTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-mexc" role="tabpanel" aria-labelledby="tracker-mexc-tab">
|
||||
<pre><code>// Either create a new factory or inject the IMexcTrackerFactory interface
|
||||
var factory = new MexcTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tracker-okx" role="tabpanel" aria-labelledby="tracker-okx-tab">
|
||||
<pre><code>// Either create a new factory or inject the IOKXTrackerFactory interface
|
||||
var factory = new OKXTrackerFactory();
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
|
||||
// Create a tracker for ETH/USDT keeping track of trades in the last 5 minutes
|
||||
var tracker = factory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5));
|
||||
var startResult = await tracker.StartAsync();
|
||||
if (!startResult.Success)
|
||||
{
|
||||
// Handle error, error info available in startResult.Error
|
||||
}
|
||||
// Tracker has successfully started
|
||||
// Note that it might not be fully synced yet, check tracker.Status for this.
|
||||
|
||||
// Once no longer needed you can stop the live sync functionality by calling StopAsync()
|
||||
await tracker.StopAsync();
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<b>Stats and comparing data</b><br />
|
||||
|
||||
Using the <code>tracker.GetData(fromTime, toTime)</code> method the trackers expose the data, or a subset of the data, can be retrieved:
|
||||
<pre><code>// Get all the data currently tracked:
|
||||
var data = tracker.GetData();
|
||||
|
||||
// Get the data for the last minute:
|
||||
var data = tracker.GetData(DateTime.UtcNow.AddMinutes(-1));
|
||||
|
||||
// Get the data for the second last minute:
|
||||
var data = tracker.GetData(DateTime.UtcNow.AddMinutes(-2), DateTime.UtcNow.AddMinutes(-1));</code></pre>
|
||||
</p>
|
||||
<p>
|
||||
In a similar way statistics about the full data, or a subset of it, can be retrieved using the <code>tracker.GetStats(fromTime, toTime)</code> method:
|
||||
<pre><code>// Get statistics on all the data:
|
||||
var stats = tracker.GetStats();
|
||||
|
||||
// Get statistics for the last minute:
|
||||
var stats = tracker.GetStats(DateTime.UtcNow.AddMinutes(-1));
|
||||
|
||||
// Get statistics for the second last minute:
|
||||
var stats = tracker.GetStats(DateTime.UtcNow.AddMinutes(-2), DateTime.UtcNow.AddMinutes(-1));</code></pre>
|
||||
|
||||
See below for an overview of what these stats include.
|
||||
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Stats can also be compared to eachother to see how much values have changed using the <code>stats.CompareTo(otherStats)</code> method:
|
||||
|
||||
<pre><code>var statsSecondLastMinute = tracker.GetStats(compareTime.AddMinutes(-2), compareTime.AddMinutes(-1));
|
||||
var statsLastMinute = tracker.GetStats(compareTime.AddMinutes(-1), compareTime);
|
||||
var comparison = statsLastMinute.CompareTo(statsSecondLastMinute);
|
||||
|
||||
Console.WriteLine($"The volume of last minute compared to the minute before that: {comparison.VolumeDif?.Difference} ({comparison.VolumeDif?.PercentageDifference}%)");
|
||||
// Output: The volume of last minute compared to the minute before that: -1261,57350000 (-85,2572%)</code></pre>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>The <code>TradeTracker</code> object</b><br />
|
||||
|
||||
</p>
|
||||
<p>
|
||||
The following properties and events are exposed by the <code>TradeTracker</code> object:
|
||||
<table class="table table-bordered">
|
||||
<tr><th>Field</th><th>Description</th></tr>
|
||||
<tr>
|
||||
<td>Count</td>
|
||||
<td>The total number of trades currently tracked</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Exchange</td>
|
||||
<td>The name of the exchange</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SymbolName</td>
|
||||
<td>The name of the symbol being tracked</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Symbol</td>
|
||||
<td>The symbol as passed in the constructor</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Limit</td>
|
||||
<td>The max number of results tracked</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Period</td>
|
||||
<td>The max age of results tracked</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SyncedFrom</td>
|
||||
<td>The timestamp from which on the trades are registered</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>The current synchronization status. Note the <code>PartiallySynced</code> means that the connection is active, but the data set is not yet complete. For example if the tracker is set to track 5 minutes of trades, it could be that currently only 3 minutes of data is tracked and it will take 2 more minutes to be fully synced.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last</td>
|
||||
<td>The current last trade</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OnAdded</td>
|
||||
<td>Event for when a new trade is added</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OnRemoved</td>
|
||||
<td>Event for when a trade is removed from the tracker due to not being within the set tracking period/limit anymore</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OnStatusChanged</td>
|
||||
<td>Event for when the status of the tracker changes</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</p>
|
||||
<p>
|
||||
|
||||
The following properties are available in the <code>TradesStats</code> object:
|
||||
<table class="table table-bordered">
|
||||
<tr><th>Field</th><th>Description</th></tr>
|
||||
<tr>
|
||||
<td>TradeCount</td>
|
||||
<td>The number trades in this data set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FirstTradeTime</td>
|
||||
<td>The timestamp of the first trade in this data set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LastTradeTime</td>
|
||||
<td>The timestamp of the last trade in this data set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AveragePrice</td>
|
||||
<td>The average price of the trades in this dataset</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VolumeWeightedAveragePrice</td>
|
||||
<td>The volume weighted average price for trades in this dataset</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Volume</td>
|
||||
<td>The total volume of all trades in this set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>QuoteVolume</td>
|
||||
<td>The total volume of all trades in this set denoted in the quote asset</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>BuySellRatio</td>
|
||||
<td>The buy sell ratio of all trades in this data set. The factor of how much of the trades were a buy.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Complete</td>
|
||||
<td>Whether the data set is complete. A set is not complete when not all data is available. For example, when only 1 hour of data is available in the tracker but stats are requested for the last 2 hours.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
<b>The <code>KlineTracker</code> object</b><br />
|
||||
|
||||
</p>
|
||||
<p>
|
||||
The following properties and events are exposed by the <code>KlineTracker</code> object:
|
||||
<table class="table table-bordered">
|
||||
<tr><th>Field</th><th>Description</th></tr>
|
||||
<tr>
|
||||
<td>Count</td>
|
||||
<td>The total number of klines currently tracked</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Exchange</td>
|
||||
<td>The name of the exchange</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SymbolName</td>
|
||||
<td>The name of the symbol being tracked</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Symbol</td>
|
||||
<td>The symbol as passed in the constructor</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Limit</td>
|
||||
<td>The max number of results tracked</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Period</td>
|
||||
<td>The max age of results tracked</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SyncedFrom</td>
|
||||
<td>The timestamp from which on the klines are registered</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>The current synchronization status. Note the <code>PartiallySynced</code> means that the connection is active, but the data set is not yet complete. For example if the tracker is set to track 10 hours of klines, it could be that currently only 9 hours of data is tracked and it will take another hour to be fully synced.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last</td>
|
||||
<td>The current last kline</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OnAdded</td>
|
||||
<td>Event for when a new kline is added</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OnRemoved</td>
|
||||
<td>Event for when a kline is removed from the tracker due to not being within the set tracking period/limit anymore</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OnStatusChanged</td>
|
||||
<td>Event for when the status of the tracker changes</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</p>
|
||||
<p>
|
||||
|
||||
The following properties are available on the <code>KlinesStats</code> object:
|
||||
<table class="table table-bordered">
|
||||
<tr><th>Field</th><th>Description</th></tr>
|
||||
<tr>
|
||||
<td>KlineCount</td>
|
||||
<td>The number klines in this data set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FirstOpenTime</td>
|
||||
<td>The open time of the first kline in this data set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LastOpenTime</td>
|
||||
<td>The open time of the last kline in this data set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LowPrice</td>
|
||||
<td>The lowest price for all klines in this set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>HighPrice</td>
|
||||
<td>The highest price for all kline in this set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Volume</td>
|
||||
<td>The total volume of all klines in this set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AverageVolume</td>
|
||||
<td>The average volume per kline for all klines in this set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Complete</td>
|
||||
<td>Whether the data set is complete. A set is not complete when not all data is available. For example, when only 1 hour of data is available in the tracker but stats are requested for the last 2 hours.</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
<!-- Layout
|
||||
============================ -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user