From cb1826da7acf730e32bbad43458662ca4e25f35a Mon Sep 17 00:00:00 2001 From: Jkorf Date: Fri, 12 Nov 2021 09:40:42 +0100 Subject: [PATCH] Documentation --- .../RestClientTests.cs | 2 +- .../Attributes/JsonConversionAttribute.cs | 1 + .../JsonOptionalPropertyAttribute.cs | 11 - .../Authentication/ApiCredentials.cs | 2 +- CryptoExchange.Net/BaseClient.cs | 5 +- .../Converters/ArrayConverter.cs | 1 + .../Converters/BaseConverter.cs | 2 +- .../Converters/TimestampConverter.cs | 2 +- CryptoExchange.Net/Interfaces/IRestClient.cs | 4 +- .../Interfaces/ISocketClient.cs | 9 +- CryptoExchange.Net/Interfaces/IWebsocket.cs | 40 +- .../Interfaces/IWebsocketFactory.cs | 12 +- CryptoExchange.Net/Logging/ConsoleLogger.cs | 2 +- CryptoExchange.Net/Logging/DebugLogger.cs | 2 +- CryptoExchange.Net/Objects/Options.cs | 18 +- .../OrderBook/ProcessBufferEntry.cs | 8 +- .../OrderBook/SymbolOrderBook.cs | 609 +++++++++--------- CryptoExchange.Net/Requests/Request.cs | 3 +- CryptoExchange.Net/Requests/RequestFactory.cs | 2 +- CryptoExchange.Net/Requests/Response.cs | 2 +- CryptoExchange.Net/RestClient.cs | 6 +- CryptoExchange.Net/SocketClient.cs | 4 +- .../Sockets/CryptoExchangeWebSocketClient.cs | 83 +-- CryptoExchange.Net/Sockets/MessageEvent.cs | 2 +- .../Sockets/SocketConnection.cs | 46 +- .../Sockets/SocketSubscription.cs | 18 +- .../Sockets/UpdateSubscription.cs | 6 +- 27 files changed, 427 insertions(+), 475 deletions(-) delete mode 100644 CryptoExchange.Net/Attributes/JsonOptionalPropertyAttribute.cs diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 6152424..aa480d6 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -116,7 +116,7 @@ namespace CryptoExchange.Net.UnitTests // assert Assert.IsTrue(client.ClientOptions.BaseAddress == "http://test.address.com/"); - Assert.IsTrue(client.ClientOptions.RateLimiters.Count() == 1); + Assert.IsTrue(client.ClientOptions.RateLimiters.Count == 1); Assert.IsTrue(client.ClientOptions.RateLimitingBehaviour == RateLimitingBehaviour.Fail); Assert.IsTrue(client.ClientOptions.RequestTimeout == TimeSpan.FromMinutes(1)); } diff --git a/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs b/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs index 67c9021..e3fdb29 100644 --- a/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs +++ b/CryptoExchange.Net/Attributes/JsonConversionAttribute.cs @@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes /// /// Used for conversion in ArrayConverter /// + [AttributeUsage(AttributeTargets.Property)] public class JsonConversionAttribute: Attribute { } diff --git a/CryptoExchange.Net/Attributes/JsonOptionalPropertyAttribute.cs b/CryptoExchange.Net/Attributes/JsonOptionalPropertyAttribute.cs deleted file mode 100644 index c50ff7b..0000000 --- a/CryptoExchange.Net/Attributes/JsonOptionalPropertyAttribute.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace CryptoExchange.Net.Attributes -{ - /// - /// Marks property as optional - /// - public class JsonOptionalPropertyAttribute : Attribute - { - } -} diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index 60c6aa6..18355c0 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -7,7 +7,7 @@ using Newtonsoft.Json.Linq; namespace CryptoExchange.Net.Authentication { /// - /// Api credentials info + /// Api credentials, used to sign requests accessing private endpoints /// public class ApiCredentials: IDisposable { diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs index aa4a652..3af741d 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/BaseClient.cs @@ -30,7 +30,7 @@ namespace CryptoExchange.Net /// protected internal Log log; /// - /// The authentication provider + /// The authentication provider when api credentials have been provided /// protected internal AuthenticationProvider? authProvider; /// @@ -72,7 +72,6 @@ namespace CryptoExchange.Net ClientOptions = options; ExchangeName = exchangeName; - //BaseAddress = options.BaseAddress; log.Write(LogLevel.Debug, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {ExchangeName}.Net: v{GetType().Assembly.GetName().Version}"); } @@ -88,7 +87,7 @@ namespace CryptoExchange.Net } /// - /// Tries to parse the json data and returns a JToken, validating the input not being empty and being valid json + /// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json /// /// The data to parse /// diff --git a/CryptoExchange.Net/Converters/ArrayConverter.cs b/CryptoExchange.Net/Converters/ArrayConverter.cs index f46cfb3..b196fd0 100644 --- a/CryptoExchange.Net/Converters/ArrayConverter.cs +++ b/CryptoExchange.Net/Converters/ArrayConverter.cs @@ -181,6 +181,7 @@ namespace CryptoExchange.Net.Converters /// /// Mark property as an index in the array /// + [AttributeUsage(AttributeTargets.Property)] public class ArrayPropertyAttribute: Attribute { /// diff --git a/CryptoExchange.Net/Converters/BaseConverter.cs b/CryptoExchange.Net/Converters/BaseConverter.cs index 75d5948..a507a18 100644 --- a/CryptoExchange.Net/Converters/BaseConverter.cs +++ b/CryptoExchange.Net/Converters/BaseConverter.cs @@ -75,7 +75,7 @@ namespace CryptoExchange.Net.Converters private bool GetValue(string value, out T result) { - //check for exact match first, then if not found fallback to a case insensitive match + // Check for exact match first, then if not found fallback to a case insensitive match var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); if(mapping.Equals(default(KeyValuePair))) mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); diff --git a/CryptoExchange.Net/Converters/TimestampConverter.cs b/CryptoExchange.Net/Converters/TimestampConverter.cs index 7dd9c21..b070771 100644 --- a/CryptoExchange.Net/Converters/TimestampConverter.cs +++ b/CryptoExchange.Net/Converters/TimestampConverter.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace CryptoExchange.Net.Converters { /// - /// converter for milliseconds to datetime + /// Converter for milliseconds to datetime /// public class TimestampConverter : JsonConverter { diff --git a/CryptoExchange.Net/Interfaces/IRestClient.cs b/CryptoExchange.Net/Interfaces/IRestClient.cs index 3778958..3c150ed 100644 --- a/CryptoExchange.Net/Interfaces/IRestClient.cs +++ b/CryptoExchange.Net/Interfaces/IRestClient.cs @@ -18,7 +18,7 @@ namespace CryptoExchange.Net.Interfaces IRequestFactory RequestFactory { get; set; } /// - /// The total amount of requests made + /// The total amount of requests made with this client /// int TotalRequestsMade { get; } @@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Interfaces void RemoveRateLimiters(); /// - /// Client options + /// The options provided for this client /// RestClientOptions ClientOptions { get; } } diff --git a/CryptoExchange.Net/Interfaces/ISocketClient.cs b/CryptoExchange.Net/Interfaces/ISocketClient.cs index 12964de..b18c835 100644 --- a/CryptoExchange.Net/Interfaces/ISocketClient.cs +++ b/CryptoExchange.Net/Interfaces/ISocketClient.cs @@ -11,7 +11,7 @@ namespace CryptoExchange.Net.Interfaces public interface ISocketClient: IDisposable { /// - /// Client options + /// The options provided for this client /// SocketClientOptions ClientOptions { get; } @@ -20,6 +20,13 @@ namespace CryptoExchange.Net.Interfaces /// public double IncomingKbps { get; } + /// + /// Unsubscribe from a stream using the subscription id received when starting the subscription + /// + /// The id of the subscription to unsubscribe + /// + Task UnsubscribeAsync(int subscriptionId); + /// /// Unsubscribe from a stream /// diff --git a/CryptoExchange.Net/Interfaces/IWebsocket.cs b/CryptoExchange.Net/Interfaces/IWebsocket.cs index 9e05ac6..af84536 100644 --- a/CryptoExchange.Net/Interfaces/IWebsocket.cs +++ b/CryptoExchange.Net/Interfaces/IWebsocket.cs @@ -7,41 +7,41 @@ using System.Threading.Tasks; namespace CryptoExchange.Net.Interfaces { /// - /// Interface for websocket interaction + /// Webscoket connection interface /// public interface IWebsocket: IDisposable { /// - /// Websocket closed + /// Websocket closed event /// event Action OnClose; /// - /// Websocket message received + /// Websocket message received event /// event Action OnMessage; /// - /// Websocket error + /// Websocket error event /// event Action OnError; /// - /// Websocket opened + /// Websocket opened event /// event Action OnOpen; /// - /// Id + /// Unique id for this socket /// int Id { get; } /// - /// Origin + /// Origin header /// string? Origin { get; set; } /// - /// Encoding to use + /// Encoding to use for sending/receiving string data /// Encoding? Encoding { get; set; } /// - /// Reconnecting + /// Whether socket is in the process of reconnecting /// bool Reconnecting { get; set; } /// @@ -61,15 +61,15 @@ namespace CryptoExchange.Net.Interfaces /// Func? DataInterpreterString { get; set; } /// - /// Socket url + /// The url the socket connects to /// string Url { get; } /// - /// Is closed + /// Whether the socket connection is closed /// bool IsClosed { get; } /// - /// Is open + /// Whether the socket connection is open /// bool IsOpen { get; } /// @@ -77,10 +77,15 @@ namespace CryptoExchange.Net.Interfaces /// SslProtocols SSLProtocols { get; set; } /// - /// Timeout + /// The max time for no data being received before the connection is considered lost /// TimeSpan Timeout { get; set; } /// + /// Set a proxy to use when connecting + /// + /// + void SetProxy(ApiProxy proxy); + /// /// Connect the socket /// /// @@ -91,18 +96,13 @@ namespace CryptoExchange.Net.Interfaces /// void Send(string data); /// - /// Reset socket + /// Reset socket when a connection is lost to prepare for a new connection /// void Reset(); /// - /// Close the connecting + /// Close the connection /// /// Task CloseAsync(); - /// - /// Set proxy - /// - /// - void SetProxy(ApiProxy proxy); } } diff --git a/CryptoExchange.Net/Interfaces/IWebsocketFactory.cs b/CryptoExchange.Net/Interfaces/IWebsocketFactory.cs index 1b0d74f..809c624 100644 --- a/CryptoExchange.Net/Interfaces/IWebsocketFactory.cs +++ b/CryptoExchange.Net/Interfaces/IWebsocketFactory.cs @@ -11,17 +11,17 @@ namespace CryptoExchange.Net.Interfaces /// /// Create a websocket for an url /// - /// - /// + /// The logger + /// The url the socket is fo /// IWebsocket CreateWebsocket(Log log, string url); /// /// Create a websocket for an url /// - /// - /// - /// - /// + /// The logger + /// The url the socket is fo + /// Cookies to be send in the initial request + /// Headers to be send in the initial request /// IWebsocket CreateWebsocket(Log log, string url, IDictionary cookies, IDictionary headers); } diff --git a/CryptoExchange.Net/Logging/ConsoleLogger.cs b/CryptoExchange.Net/Logging/ConsoleLogger.cs index baabb7d..54d67a4 100644 --- a/CryptoExchange.Net/Logging/ConsoleLogger.cs +++ b/CryptoExchange.Net/Logging/ConsoleLogger.cs @@ -4,7 +4,7 @@ using System; namespace CryptoExchange.Net.Logging { /// - /// Log to console + /// ILogger implementation for logging to the console /// public class ConsoleLogger : ILogger { diff --git a/CryptoExchange.Net/Logging/DebugLogger.cs b/CryptoExchange.Net/Logging/DebugLogger.cs index 88b8f7d..9b042de 100644 --- a/CryptoExchange.Net/Logging/DebugLogger.cs +++ b/CryptoExchange.Net/Logging/DebugLogger.cs @@ -5,7 +5,7 @@ using System.Diagnostics; namespace CryptoExchange.Net.Logging { /// - /// Default log writer, writes to debug + /// Default log writer, uses Trace.WriteLine /// public class DebugLogger: ILogger { diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index 45c22f4..24c5f58 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Objects public class BaseOptions { /// - /// The minimum log level to output. Setting it to null will send all messages to the registered ILoggers. + /// The minimum log level to output /// public LogLevel LogLevel { get; set; } = LogLevel.Information; @@ -86,12 +86,12 @@ namespace CryptoExchange.Net.Objects } /// - /// The api credentials + /// The api credentials used for signing requests /// public ApiCredentials? ApiCredentials { get; set; } /// - /// Proxy to use + /// Proxy to use when connecting /// public ApiProxy? Proxy { get; set; } @@ -161,7 +161,7 @@ namespace CryptoExchange.Net.Objects /// public override string ToString() { - return $"{base.ToString()}, RateLimiters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout:c}"; + return $"{base.ToString()}, RateLimiters: {RateLimiters.Count}, RateLimitBehaviour: {RateLimitingBehaviour}, RequestTimeout: {RequestTimeout:c}, HttpClient: {(HttpClient == null ? "-": "set")}"; } } @@ -181,7 +181,7 @@ namespace CryptoExchange.Net.Objects public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5); /// - /// The maximum number of times to try to reconnect + /// The maximum number of times to try to reconnect, default null will retry indefinitely /// public int? MaxReconnectTries { get; set; } @@ -196,11 +196,13 @@ namespace CryptoExchange.Net.Objects public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5; /// - /// The time to wait for a socket response before giving a timeout + /// The max time to wait for a response after sending a request on the socket before giving a timeout /// public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10); + /// - /// The time after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected. + /// The max time of not receiving any data after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected, + /// for example when the server sends intermittent ping requests /// public TimeSpan SocketNoDataTimeout { get; set; } @@ -234,7 +236,7 @@ namespace CryptoExchange.Net.Objects /// public override string ToString() { - return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}"; + return $"{base.ToString()}, AutoReconnect: {AutoReconnect}, ReconnectInterval: {ReconnectInterval}, MaxReconnectTries: {MaxReconnectTries}, MaxResubscribeTries: {MaxResubscribeTries}, MaxConcurrentResubscriptionsPerSocket: {MaxConcurrentResubscriptionsPerSocket}, SocketResponseTimeout: {SocketResponseTimeout:c}, SocketNoDataTimeout: {SocketNoDataTimeout}, SocketSubscriptionsCombineTarget: {SocketSubscriptionsCombineTarget}"; } } } diff --git a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs index 1bf9a60..c907881 100644 --- a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs +++ b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs @@ -10,19 +10,19 @@ namespace CryptoExchange.Net.OrderBook public class ProcessBufferRangeSequenceEntry { /// - /// First update id + /// First sequence number in this update /// public long FirstUpdateId { get; set; } /// - /// Last update id + /// Last sequence number in this update /// public long LastUpdateId { get; set; } /// - /// List of asks + /// List of changed/new asks /// public IEnumerable Asks { get; set; } = Array.Empty(); /// - /// List of bids + /// List of changed/new bids /// public IEnumerable Bids { get; set; } = Array.Empty(); } diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 17d9136..a7f1e29 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -18,52 +18,64 @@ namespace CryptoExchange.Net.OrderBook /// public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable { - /// - /// The process buffer, used while syncing - /// - protected readonly List processBuffer; - /// - /// The ask list - /// - protected SortedList asks; - /// - /// The bid list - /// - protected SortedList bids; - private readonly object bookLock = new object(); private OrderBookStatus status; private UpdateSubscription? subscription; - private readonly bool validateChecksum; - private bool _stopProcessing; private Task? _processTask; + private readonly AutoResetEvent _queueEvent; private readonly ConcurrentQueue _processQueue; + private readonly bool validateChecksum; + + private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry + { + public decimal Quantity { get { return 0m; } set {; } } + public decimal Price { get { return 0m; } set {; } } + } + + private static readonly ISymbolOrderBookEntry emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry(); + /// - /// Order book implementation id + /// A buffer to store messages received before the initial book snapshot is processed. These messages + /// will be processed after the book snapshot is set. Any messages in this buffer with sequence numbers lower + /// than the snapshot sequence number will be discarded /// - public string Id { get; } + protected readonly List processBuffer; + + /// + /// The ask list, should only be accessed using the bookLock + /// + protected SortedList asks; + + /// + /// The bid list, should only be accessed using the bookLock + /// + protected SortedList bids; + /// /// The log /// protected Log log; /// - /// Whether update numbers are consecutive + /// Whether update numbers are consecutive. If set to true and an update comes in which isn't the previous sequences number + 1 + /// the book will resynchronize as it is deemed out of sync /// protected bool sequencesAreConsecutive; /// - /// Whether levels should be strictly enforced + /// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes + /// the current level 25 ask out of the top 25, should the curent the level 26 entry be removed from the book or does the + /// server handle this /// protected bool strictLevels; /// - /// If order book is set + /// If the initial snapshot of the book has been set /// protected bool bookSet; @@ -72,9 +84,10 @@ namespace CryptoExchange.Net.OrderBook /// protected int? Levels { get; set; } = null; - /// - /// The status of the order book. Order book is up to date when the status is `Synced` - /// + /// + public string Id { get; } + + /// public OrderBookStatus Status { get => status; @@ -90,46 +103,31 @@ namespace CryptoExchange.Net.OrderBook } } - /// - /// Last update identifier - /// + /// public long LastSequenceNumber { get; private set; } - /// - /// The symbol of the order book - /// + + /// public string Symbol { get; } - /// - /// Event when the state changes - /// + /// public event Action? OnStatusChange; - /// - /// Event when the BestBid or BestAsk changes ie a Pricing Tick - /// + /// public event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)>? OnBestOffersChanged; - /// - /// Event when order book was updated, containing the changed bids and asks. Be careful! It can generate a lot of events at high-liquidity markets - /// + /// public event Action<(IEnumerable Bids, IEnumerable Asks)>? OnOrderBookUpdate; - /// - /// Timestamp of the last update - /// + + /// public DateTime UpdateTime { get; private set; } - /// - /// The number of asks in the book - /// + /// public int AskCount { get; private set; } - /// - /// The number of bids in the book - /// + + /// public int BidCount { get; private set; } - /// - /// The list of asks - /// + /// public IEnumerable Asks { get @@ -139,9 +137,7 @@ namespace CryptoExchange.Net.OrderBook } } - /// - /// The list of bids - /// + /// public IEnumerable Bids { get @@ -151,9 +147,7 @@ namespace CryptoExchange.Net.OrderBook } } - /// - /// Get a snapshot of the book at this moment - /// + /// public (IEnumerable bids, IEnumerable asks) Book { get @@ -163,17 +157,7 @@ namespace CryptoExchange.Net.OrderBook } } - private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry - { - public decimal Quantity { get { return 0m; } set {; } } - public decimal Price { get { return 0m; } set {; } } - } - - private static readonly ISymbolOrderBookEntry emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry(); - - /// - /// The best bid currently in the order book - /// + /// public ISymbolOrderBookEntry BestBid { get @@ -183,9 +167,7 @@ namespace CryptoExchange.Net.OrderBook } } - /// - /// The best ask currently in the order book - /// + /// public ISymbolOrderBookEntry BestAsk { get @@ -195,9 +177,7 @@ namespace CryptoExchange.Net.OrderBook } } - /// - /// BestBid/BesAsk returned as a pair - /// + /// public (ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers { get { lock (bookLock) @@ -208,9 +188,9 @@ namespace CryptoExchange.Net.OrderBook /// /// ctor /// - /// - /// - /// + /// The id of the order book. Should be set to {Exchange}[{type}], for example: Kucoin[Spot] + /// The symbol the order book is for + /// The options for the order book protected SymbolOrderBook(string id, string symbol, OrderBookOptions options) { if (symbol == null) @@ -236,10 +216,7 @@ namespace CryptoExchange.Net.OrderBook log.UpdateWriters(writers.ToList()); } - /// - /// Start connecting and synchronizing the order book - /// - /// + /// public async Task> StartAsync() { if (Status != OrderBookStatus.Disconnected) @@ -275,13 +252,21 @@ namespace CryptoExchange.Net.OrderBook return new CallResult(true, null); } - /// - /// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled - /// at that price since between this calculation and the order placement the book can have changed. - /// - /// The quantity in base asset to fill - /// The type - /// Average fill price + /// + public async Task StopAsync() + { + log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopping"); + Status = OrderBookStatus.Disconnected; + _queueEvent.Set(); + if (_processTask != null) + await _processTask.ConfigureAwait(false); + + if (subscription != null) + await subscription.CloseAsync().ConfigureAwait(false); + log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopped"); + } + + /// public CallResult CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type) { if (Status != OrderBookStatus.Synced) @@ -312,6 +297,219 @@ namespace CryptoExchange.Net.OrderBook return new CallResult(Math.Round(totalCost / totalAmount, 8), null); } + /// + /// Implementation for starting the order book. Should typically have logic for subscribing to the update stream and retrieving + /// and setting the initial order book + /// + /// + protected abstract Task> DoStartAsync(); + + /// + /// Reset the order book + /// + protected virtual void DoReset() { } + + /// + /// Resync the order book + /// + /// + protected abstract Task> DoResyncAsync(); + + /// + /// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false) + /// the order book will be resynchronized + /// + /// + /// + protected virtual bool DoChecksum(int checksum) => true; + + /// + /// Set the initial data for the order book. Typically the snapshot which was requested from the Rest API, or the first snapshot + /// received from a socket subcription + /// + /// The last update sequence number until which the snapshot is in sync + /// List of asks + /// List of bids + protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable bidList, IEnumerable askList) + { + _processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList }); + _queueEvent.Set(); + } + + /// + /// Add an update to the process queue. Updates the book by providing changed bids and asks, along with an update number which should be higher than the previous update numbers + /// + /// The sequence number + /// List of updated/new bids + /// List of updated/new asks + protected void UpdateOrderBook(long updateId, IEnumerable bids, IEnumerable asks) + { + _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = updateId, EndUpdateId = updateId, Asks = asks, Bids = bids }); + _queueEvent.Set(); + } + + /// + /// Add an update to the process queue. Updates the book by providing changed bids and asks, along with the first and last sequence number in the update + /// + /// The sequence number of the first update + /// The sequence number of the last update + /// List of updated/new bids + /// List of updated/new asks + protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable bids, IEnumerable asks) + { + _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids }); + _queueEvent.Set(); + } + + /// + /// Add an update to the process queue. Updates the book by providing changed bids and asks, each with its own sequence number + /// + /// List of updated/new bids + /// List of updated/new asks + protected void UpdateOrderBook(IEnumerable bids, IEnumerable asks) + { + var highest = Math.Max(bids.Any() ? bids.Max(b => b.Sequence) : 0, asks.Any() ? asks.Max(a => a.Sequence) : 0); + var lowest = Math.Min(bids.Any() ? bids.Min(b => b.Sequence) : long.MaxValue, asks.Any() ? asks.Min(a => a.Sequence) : long.MaxValue); + + _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = lowest, EndUpdateId = highest, Asks = asks, Bids = bids }); + _queueEvent.Set(); + } + + /// + /// Add a checksum value to the process queue + /// + /// The checksum value + protected void AddChecksum(int checksum) + { + _processQueue.Enqueue(new ChecksumItem() { Checksum = checksum }); + _queueEvent.Set(); + } + + /// + /// Check and empty the process buffer; see what entries to update the book with + /// + protected void CheckProcessBuffer() + { + var pbList = processBuffer.ToList(); + if (pbList.Count > 0) + log.Write(LogLevel.Debug, "Processing buffered updates"); + + foreach (var bufferEntry in pbList) + { + ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks); + processBuffer.Remove(bufferEntry); + } + } + + /// + /// Update order book with an entry + /// + /// Sequence number of the update + /// Type of entry + /// The entry + protected virtual bool ProcessUpdate(long sequence, OrderBookEntryType type, ISymbolOrderBookEntry entry) + { + if (sequence <= LastSequenceNumber) + { + log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}"); + return false; + } + + if (sequencesAreConsecutive && sequence > LastSequenceNumber + 1) + { + // Out of sync + log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting"); + _stopProcessing = true; + Resubscribe(); + return false; + } + + UpdateTime = DateTime.UtcNow; + var listToChange = type == OrderBookEntryType.Ask ? asks : bids; + if (entry.Quantity == 0) + { + if (!listToChange.ContainsKey(entry.Price)) + return true; + + listToChange.Remove(entry.Price); + if (type == OrderBookEntryType.Ask) AskCount--; + else BidCount--; + } + else + { + if (!listToChange.ContainsKey(entry.Price)) + { + listToChange.Add(entry.Price, entry); + if (type == OrderBookEntryType.Ask) AskCount++; + else BidCount++; + } + else + { + listToChange[entry.Price] = entry; + } + } + + return true; + } + + /// + /// Wait until the order book snapshot has been set + /// + /// Max wait time + /// + protected async Task> WaitForSetOrderBookAsync(int timeout) + { + var startWait = DateTime.UtcNow; + while (!bookSet && Status == OrderBookStatus.Syncing) + { + if ((DateTime.UtcNow - startWait).TotalMilliseconds > timeout) + return new CallResult(false, new ServerError("Timeout while waiting for data")); + + await Task.Delay(10).ConfigureAwait(false); + } + + return new CallResult(true, null); + } + + /// + /// Dispose the order book + /// + public abstract void Dispose(); + + /// + /// String representation of the top 3 entries + /// + /// + public override string ToString() + { + return ToString(3); + } + + /// + /// String representation of the top x entries + /// + /// + public string ToString(int numberOfEntries) + { + var result = string.Empty; + result += $"Asks ({AskCount}): {Environment.NewLine}"; + foreach (var entry in Asks.Take(numberOfEntries).Reverse()) + result += $" {entry.Price.ToString(CultureInfo.InvariantCulture).PadLeft(8)} | {entry.Quantity.ToString(CultureInfo.InvariantCulture).PadRight(8)}{Environment.NewLine}"; + + result += $"Bids ({BidCount}): {Environment.NewLine}"; + foreach (var entry in Bids.Take(numberOfEntries)) + result += $" {entry.Price.ToString(CultureInfo.InvariantCulture).PadLeft(8)} | {entry.Quantity.ToString(CultureInfo.InvariantCulture).PadRight(8)}{Environment.NewLine}"; + return result; + } + + private void CheckBestOffersChanged(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk) + { + var (bestBid, bestAsk) = BestOffers; + if (bestBid.Price != prevBestBid.Price || bestBid.Quantity != prevBestBid.Quantity || + bestAsk.Price != prevBestAsk.Price || bestAsk.Quantity != prevBestAsk.Quantity) + OnBestOffersChanged?.Invoke((bestBid, bestAsk)); + } + private void Reset() { _queueEvent.Set(); @@ -339,50 +537,9 @@ namespace CryptoExchange.Net.OrderBook Status = OrderBookStatus.Synced; } - /// - /// Stop syncing the order book - /// - /// - public async Task StopAsync() - { - log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopping"); - Status = OrderBookStatus.Disconnected; - _queueEvent.Set(); - if(_processTask != null) - await _processTask.ConfigureAwait(false); - - if(subscription != null) - await subscription.CloseAsync().ConfigureAwait(false); - log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopped"); - } - - /// - /// Start the order book - /// - /// - protected abstract Task> DoStartAsync(); - - /// - /// Reset the order book - /// - protected virtual void DoReset() { } - - /// - /// Resync the order book - /// - /// - protected abstract Task> DoResyncAsync(); - - /// - /// Validate a checksum with the current order book - /// - /// - /// - protected virtual bool DoChecksum(int checksum) => true; - private void ProcessQueue() { - while(Status != OrderBookStatus.Disconnected) + while (Status != OrderBookStatus.Disconnected) { _queueEvent.WaitOne(); @@ -462,7 +619,7 @@ namespace CryptoExchange.Net.OrderBook _stopProcessing = true; Resubscribe(); return; - } + } OnOrderBookUpdate?.Invoke((item.Bids, item.Asks)); CheckBestOffersChanged(prevBestBid, prevBestAsk); @@ -476,7 +633,7 @@ namespace CryptoExchange.Net.OrderBook { if (!validateChecksum) return; - + bool checksumResult = false; try { @@ -490,7 +647,7 @@ namespace CryptoExchange.Net.OrderBook throw; } - if(!checksumResult) + if (!checksumResult) { log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync. Resyncing"); _stopProcessing = true; @@ -520,67 +677,6 @@ namespace CryptoExchange.Net.OrderBook }); } - /// - /// Set the initial data for the order book - /// - /// The last update sequence number - /// List of asks - /// List of bids - protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable bidList, IEnumerable askList) - { - _processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList }); - _queueEvent.Set(); - } - - /// - /// Update the order book using a single id for an update - /// - /// - /// - /// - protected void UpdateOrderBook(long rangeUpdateId, IEnumerable bids, IEnumerable asks) - { - _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = rangeUpdateId, EndUpdateId = rangeUpdateId, Asks = asks, Bids = bids }); - _queueEvent.Set(); - } - - /// - /// Add a checksum to the process queue - /// - /// - protected void AddChecksum(int checksum) - { - _processQueue.Enqueue(new ChecksumItem() { Checksum = checksum }); - _queueEvent.Set(); - } - - /// - /// Update the order book using a first/last update id - /// - /// - /// - /// - /// - protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable bids, IEnumerable asks) - { - _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids }); - _queueEvent.Set(); - } - - /// - /// Update the order book using sequenced entries - /// - /// List of bids - /// List of asks - protected void UpdateOrderBook(IEnumerable bids, IEnumerable asks) - { - var highest = Math.Max(bids.Any() ? bids.Max(b => b.Sequence) : 0, asks.Any() ? asks.Max(a => a.Sequence) : 0); - var lowest = Math.Min(bids.Any() ? bids.Min(b => b.Sequence) : long.MaxValue, asks.Any() ? asks.Min(a => a.Sequence) : long.MaxValue); - - _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = lowest, EndUpdateId = highest , Asks = asks, Bids = bids }); - _queueEvent.Set(); - } - private void ProcessRangeUpdates(long firstUpdateId, long lastUpdateId, IEnumerable bids, IEnumerable asks) { if (lastUpdateId <= LastSequenceNumber) @@ -612,132 +708,7 @@ namespace CryptoExchange.Net.OrderBook LastSequenceNumber = lastUpdateId; log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}"); - } - - /// - /// Check and empty the process buffer; see what entries to update the book with - /// - protected void CheckProcessBuffer() - { - var pbList = processBuffer.ToList(); - if(pbList.Count > 0) - log.Write(LogLevel.Debug, "Processing buffered updates"); - - foreach (var bufferEntry in pbList) - { - ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks); - processBuffer.Remove(bufferEntry); - } - } - - /// - /// Update order book with an entry - /// - /// Sequence number of the update - /// Type of entry - /// The entry - protected virtual bool ProcessUpdate(long sequence, OrderBookEntryType type, ISymbolOrderBookEntry entry) - { - if (sequence <= LastSequenceNumber) - { - log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}"); - return false; - } - - if (sequencesAreConsecutive && sequence > LastSequenceNumber + 1) - { - // Out of sync - log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting"); - _stopProcessing = true; - Resubscribe(); - return false; - } - - UpdateTime = DateTime.UtcNow; - var listToChange = type == OrderBookEntryType.Ask ? asks : bids; - if (entry.Quantity == 0) - { - if (!listToChange.ContainsKey(entry.Price)) - return true; - - listToChange.Remove(entry.Price); - if (type == OrderBookEntryType.Ask) AskCount--; - else BidCount--; - } - else - { - if (!listToChange.ContainsKey(entry.Price)) - { - listToChange.Add(entry.Price, entry); - if (type == OrderBookEntryType.Ask) AskCount++; - else BidCount++; - } - else - { - listToChange[entry.Price] = entry; - } - } - - return true; - } - - /// - /// Wait until the order book has been set - /// - /// Max wait time - /// - protected async Task> WaitForSetOrderBookAsync(int timeout) - { - var startWait = DateTime.UtcNow; - while (!bookSet && Status == OrderBookStatus.Syncing) - { - if ((DateTime.UtcNow - startWait).TotalMilliseconds > timeout) - return new CallResult(false, new ServerError("Timeout while waiting for data")); - - await Task.Delay(10).ConfigureAwait(false); - } - - return new CallResult(true, null); - } - - private void CheckBestOffersChanged(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk) - { - var (bestBid, bestAsk) = BestOffers; - if (bestBid.Price != prevBestBid.Price || bestBid.Quantity != prevBestBid.Quantity || - bestAsk.Price != prevBestAsk.Price || bestAsk.Quantity != prevBestAsk.Quantity) - OnBestOffersChanged?.Invoke((bestBid, bestAsk)); - } - - /// - /// Dispose the order book - /// - public abstract void Dispose(); - - /// - /// String representation of the top 3 entries - /// - /// - public override string ToString() - { - return ToString(3); - } - - /// - /// String representation of the top x entries - /// - /// - public string ToString(int numberOfEntries) - { - var result = string.Empty; - result += $"Asks ({AskCount}): {Environment.NewLine}"; - foreach (var entry in Asks.Take(numberOfEntries).Reverse()) - result += $" {entry.Price.ToString(CultureInfo.InvariantCulture).PadLeft(8)} | {entry.Quantity.ToString(CultureInfo.InvariantCulture).PadRight(8)}{Environment.NewLine}"; - - result += $"Bids ({BidCount}): {Environment.NewLine}"; - foreach (var entry in Bids.Take(numberOfEntries)) - result += $" {entry.Price.ToString(CultureInfo.InvariantCulture).PadLeft(8)} | {entry.Quantity.ToString(CultureInfo.InvariantCulture).PadRight(8)}{Environment.NewLine}"; - return result; - } + } } internal class DescComparer : IComparer diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index 58c2d44..b707530 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -11,7 +11,7 @@ using CryptoExchange.Net.Interfaces; namespace CryptoExchange.Net.Requests { /// - /// Request object + /// Request object, wrapper for HttpRequestMessage /// public class Request : IRequest { @@ -49,6 +49,7 @@ namespace CryptoExchange.Net.Requests /// public Uri Uri => request.RequestUri; + /// public int RequestId { get; } diff --git a/CryptoExchange.Net/Requests/RequestFactory.cs b/CryptoExchange.Net/Requests/RequestFactory.cs index be68097..3da74c4 100644 --- a/CryptoExchange.Net/Requests/RequestFactory.cs +++ b/CryptoExchange.Net/Requests/RequestFactory.cs @@ -7,7 +7,7 @@ using CryptoExchange.Net.Objects; namespace CryptoExchange.Net.Requests { /// - /// WebRequest factory + /// Request factory /// public class RequestFactory : IRequestFactory { diff --git a/CryptoExchange.Net/Requests/Response.cs b/CryptoExchange.Net/Requests/Response.cs index 93d2113..e5a07b4 100644 --- a/CryptoExchange.Net/Requests/Response.cs +++ b/CryptoExchange.Net/Requests/Response.cs @@ -8,7 +8,7 @@ using CryptoExchange.Net.Interfaces; namespace CryptoExchange.Net.Requests { /// - /// HttpWebResponse response object + /// Response object, wrapper for HttpResponseMessage /// internal class Response : IResponse { diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index 2ded49b..559c0b7 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -67,9 +67,7 @@ namespace CryptoExchange.Net /// protected IEnumerable RateLimiters { get; private set; } - /// - /// Total requests made by this client - /// + /// public int TotalRequestsMade { get; private set; } /// @@ -133,7 +131,6 @@ namespace CryptoExchange.Net /// Cancellation token /// The parameters of the request /// Whether or not the request should be authenticated - /// Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug) /// Where the parameters should be placed, overwrites the value set in the client /// How array parameters should be serialized, overwrites the value set in the client /// Credits used for the request @@ -147,7 +144,6 @@ namespace CryptoExchange.Net CancellationToken cancellationToken, Dictionary? parameters = null, bool signed = false, - bool checkResult = true, HttpMethodParameterPosition? parameterPosition = null, ArrayParametersSerialization? arraySerialization = null, int credits = 1, diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index 714e385..7022bd3 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -80,9 +80,7 @@ namespace CryptoExchange.Net /// protected internal int? RateLimitPerSocketPerSecond { get; set; } - /// - /// The current kilobytes per second of data being received by all connection from this client, averaged over the last 3 seconds - /// + /// public double IncomingKbps { get diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs index d3ee1cc..bc74c74 100644 --- a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs @@ -42,7 +42,7 @@ namespace CryptoExchange.Net.Sockets private DateTime _lastReceivedMessagesUpdate; /// - /// Received messages time -> size + /// Received messages, the size and the timstamp /// protected readonly List _receivedMessages; /// @@ -72,17 +72,15 @@ namespace CryptoExchange.Net.Sockets /// protected readonly List> messageHandlers = new List>(); - /// - /// The id of this socket - /// + /// public int Id { get; } /// public string? Origin { get; set; } - /// - /// Whether this socket is currently reconnecting - /// + + /// public bool Reconnecting { get; set; } + /// /// The timestamp this socket has been active for the last time /// @@ -92,22 +90,19 @@ namespace CryptoExchange.Net.Sockets /// Delegate used for processing byte data received from socket connections before it is processed by handlers /// public Func? DataInterpreterBytes { get; set; } + /// /// Delegate used for processing string data received from socket connections before it is processed by handlers /// public Func? DataInterpreterString { get; set; } - /// - /// Url this socket connects to - /// + + /// public string Url { get; } - /// - /// If the connection is closed - /// + + /// public bool IsClosed => _socket.State == WebSocketState.Closed; - /// - /// If the connection is open - /// + /// public bool IsOpen => _socket.State == WebSocketState.Open && !_closing; /// @@ -116,9 +111,7 @@ namespace CryptoExchange.Net.Sockets public SslProtocols SSLProtocols { get; set; } private Encoding _encoding = Encoding.UTF8; - /// - /// Encoding used for decoding the received bytes into a string - /// + /// public Encoding? Encoding { get => _encoding; @@ -128,19 +121,16 @@ namespace CryptoExchange.Net.Sockets _encoding = value; } } + /// /// The max amount of outgoing messages per second /// public int? RatelimitPerSecond { get; set; } - /// - /// The timespan no data is received on the socket. If no data is received within this time an error is generated - /// + /// public TimeSpan Timeout { get; set; } - /// - /// The current kilobytes per second of data being received, averaged over the last 3 seconds - /// + /// public double IncomingKbps { get @@ -157,33 +147,28 @@ namespace CryptoExchange.Net.Sockets } } - /// - /// Socket closed event - /// + /// public event Action OnClose { add => closeHandlers.Add(value); remove => closeHandlers.Remove(value); } - /// - /// Socket message received event - /// + + /// public event Action OnMessage { add => messageHandlers.Add(value); remove => messageHandlers.Remove(value); } - /// - /// Socket error event - /// + + /// public event Action OnError { add => errorHandlers.Add(value); remove => errorHandlers.Remove(value); } - /// - /// Socket opened event - /// + + /// public event Action OnOpen { add => openHandlers.Add(value); @@ -224,10 +209,7 @@ namespace CryptoExchange.Net.Sockets _socket = CreateSocket(); } - /// - /// Set a proxy to use. Should be set before connecting - /// - /// + /// public virtual void SetProxy(ApiProxy proxy) { _socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port); @@ -235,10 +217,7 @@ namespace CryptoExchange.Net.Sockets _socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password); } - /// - /// Connect the websocket - /// - /// True if successfull + /// public virtual async Task ConnectAsync() { log.Write(LogLevel.Debug, $"Socket {Id} connecting"); @@ -270,10 +249,7 @@ namespace CryptoExchange.Net.Sockets return true; } - /// - /// Send data over the websocket - /// - /// Data to send + /// public virtual void Send(string data) { if (_closing) @@ -285,10 +261,7 @@ namespace CryptoExchange.Net.Sockets _sendEvent.Set(); } - /// - /// Close the websocket - /// - /// + /// public virtual async Task CloseAsync() { log.Write(LogLevel.Debug, $"Socket {Id} closing"); @@ -344,9 +317,7 @@ namespace CryptoExchange.Net.Sockets log.Write(LogLevel.Trace, $"Socket {Id} disposed"); } - /// - /// Reset the socket so a new connection can be attempted after it has been connected before - /// + /// public void Reset() { log.Write(LogLevel.Debug, $"Socket {Id} resetting"); diff --git a/CryptoExchange.Net/Sockets/MessageEvent.cs b/CryptoExchange.Net/Sockets/MessageEvent.cs index 02a3792..b60c62c 100644 --- a/CryptoExchange.Net/Sockets/MessageEvent.cs +++ b/CryptoExchange.Net/Sockets/MessageEvent.cs @@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Sockets public DateTime ReceivedTimestamp { get; set; } /// - /// + /// ctor /// /// /// diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 1df5dc9..829a2b5 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -14,7 +14,7 @@ using CryptoExchange.Net.Objects; namespace CryptoExchange.Net.Sockets { /// - /// Socket connecting + /// A single socket connection to the server /// public class SocketConnection { @@ -22,26 +22,32 @@ namespace CryptoExchange.Net.Sockets /// Connection lost event /// public event Action? ConnectionLost; + /// /// Connection closed and no reconnect is happening /// public event Action? ConnectionClosed; + /// /// Connecting restored event /// public event Action? ConnectionRestored; + /// /// The connection is paused event /// public event Action? ActivityPaused; + /// /// The connection is unpaused event /// public event Action? ActivityUnpaused; + /// /// Connecting closed event /// public event Action? Closed; + /// /// Unhandled message event /// @@ -57,30 +63,35 @@ namespace CryptoExchange.Net.Sockets } /// - /// If connection is authenticated + /// If the connection has been authenticated /// public bool Authenticated { get; set; } + /// /// If connection is made /// public bool Connected { get; private set; } /// - /// The underlying socket + /// The underlying websocket /// public IWebsocket Socket { get; set; } + /// /// If the socket should be reconnected upon closing /// public bool ShouldReconnect { get; set; } + /// - /// Current reconnect try + /// Current reconnect try, reset when a successful connection is made /// public int ReconnectTry { get; set; } + /// - /// Current resubscribe try + /// Current resubscribe try, reset when a successful connection is made /// public int ResubscribeTry { get; set; } + /// /// Time of disconnecting /// @@ -138,7 +149,7 @@ namespace CryptoExchange.Net.Sockets /// /// Process a message received by the socket /// - /// + /// The received data private void ProcessMessage(string data) { var timestamp = DateTime.UtcNow; @@ -193,7 +204,7 @@ namespace CryptoExchange.Net.Sockets } /// - /// Add subscription to this connection + /// Add a subscription to this connection /// /// public void AddSubscription(SocketSubscription subscription) @@ -203,15 +214,20 @@ namespace CryptoExchange.Net.Sockets } /// - /// Get a subscription on this connection + /// Get a subscription on this connection by id /// /// - public SocketSubscription GetSubscription(int id) + public SocketSubscription? GetSubscription(int id) { lock (subscriptionLock) return subscriptions.SingleOrDefault(s => s.Id == id); } + /// + /// Process data + /// + /// + /// True if the data was successfully handled private bool HandleData(MessageEvent messageEvent) { SocketSubscription? currentSubscription = null; @@ -249,7 +265,7 @@ namespace CryptoExchange.Net.Sockets sw.Stop(); if (sw.ElapsedMilliseconds > 500) - log.Write(LogLevel.Warning, $"Socket {Socket.Id} message processing slow ({sw.ElapsedMilliseconds}ms), consider offloading data handling to another thread. " + + log.Write(LogLevel.Debug, $"Socket {Socket.Id} message processing slow ({sw.ElapsedMilliseconds}ms), consider offloading data handling to another thread. " + "Data from this socket may arrive late or not at all if message processing is continuously slow."); else log.Write(LogLevel.Trace, $"Socket {Socket.Id} message processed in {sw.ElapsedMilliseconds}ms"); @@ -269,7 +285,7 @@ namespace CryptoExchange.Net.Sockets /// The data type expected in response /// The object to send /// The timeout for response - /// The response handler + /// The response handler, should return true if the received JToken was the response to the request /// public virtual Task SendAndWaitAsync(T obj, TimeSpan timeout, Func handler) { @@ -391,6 +407,7 @@ namespace CryptoExchange.Net.Sockets if (!reconnectResult) { ResubscribeTry++; + DisconnectTime = time; if (socketClient.ClientOptions.MaxResubscribeTries != null && ResubscribeTry >= socketClient.ClientOptions.MaxResubscribeTries) @@ -419,7 +436,7 @@ namespace CryptoExchange.Net.Sockets if (lostTriggered) { lostTriggered = false; - InvokeConnectionRestored(time); + _ = Task.Run(() => ConnectionRestored?.Invoke(time.HasValue ? DateTime.UtcNow - time.Value : TimeSpan.FromSeconds(0))).ConfigureAwait(false); } break; @@ -443,11 +460,6 @@ namespace CryptoExchange.Net.Sockets } } - private async void InvokeConnectionRestored(DateTime? disconnectTime) - { - await Task.Run(() => ConnectionRestored?.Invoke(disconnectTime.HasValue ? DateTime.UtcNow - disconnectTime.Value : TimeSpan.FromSeconds(0))).ConfigureAwait(false); - } - private async Task ProcessReconnectAsync() { if (Authenticated) diff --git a/CryptoExchange.Net/Sockets/SocketSubscription.cs b/CryptoExchange.Net/Sockets/SocketSubscription.cs index 77058ce..23e6428 100644 --- a/CryptoExchange.Net/Sockets/SocketSubscription.cs +++ b/CryptoExchange.Net/Sockets/SocketSubscription.cs @@ -9,9 +9,10 @@ namespace CryptoExchange.Net.Sockets public class SocketSubscription { /// - /// Subscription id + /// Unique subscription id /// public int Id { get; } + /// /// Exception event /// @@ -23,25 +24,28 @@ namespace CryptoExchange.Net.Sockets public Action MessageHandler { get; set; } /// - /// Request object + /// The request object send when subscribing on the server. Either this or the `Identifier` property should be set /// public object? Request { get; set; } + /// - /// Subscription identifier + /// The subscription identifier, used instead of a `Request` object to identify the subscription /// public string? Identifier { get; set; } + /// - /// Is user subscription or generic + /// Whether this is a user subscription or an internal listener /// public bool UserSubscription { get; set; } /// - /// If the subscription has been confirmed + /// If the subscription has been confirmed to be subscribed by the server /// public bool Confirmed { get; set; } /// - /// Cancellation token registration, should be disposed when subscription is closed + /// Cancellation token registration, should be disposed when subscription is closed. Used for closing the subscription with + /// a provided cancelation token /// public CancellationTokenRegistration? CancellationTokenRegistration { get; set; } @@ -55,7 +59,7 @@ namespace CryptoExchange.Net.Sockets } /// - /// Create SocketSubscription for a request + /// Create SocketSubscription for a subscribe request /// /// /// diff --git a/CryptoExchange.Net/Sockets/UpdateSubscription.cs b/CryptoExchange.Net/Sockets/UpdateSubscription.cs index dd8b029..39fa5d0 100644 --- a/CryptoExchange.Net/Sockets/UpdateSubscription.cs +++ b/CryptoExchange.Net/Sockets/UpdateSubscription.cs @@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Sockets /// /// Event when the connection is closed. This event happens when reconnecting/resubscribing has failed too often based on the and options, - /// or is false + /// or is false. The socket will not be reconnected /// public event Action ConnectionClosed { @@ -33,8 +33,8 @@ namespace CryptoExchange.Net.Sockets /// /// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting. - /// Note that when the executing code is suspended and resumed at a later period (for example laptop going to sleep) the disconnect time will be incorrect as the diconnect - /// will only be detected after resuming. This will lead to an incorrect disconnected timespan. + /// Note that when the executing code is suspended and resumed at a later period (for example, a laptop going to sleep) the disconnect time will be incorrect as the diconnect + /// will only be detected after resuming the code, so the initial disconnect time is lost. Use the timespan only for informational purposes. /// public event Action ConnectionRestored {