1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-06-09 17:06:19 +00:00

Documentation

This commit is contained in:
Jkorf 2021-11-12 09:40:42 +01:00
parent f7445543f2
commit cb1826da7a
27 changed files with 427 additions and 475 deletions

View File

@ -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));
}

View File

@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes
/// <summary>
/// Used for conversion in ArrayConverter
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class JsonConversionAttribute: Attribute
{
}

View File

@ -1,11 +0,0 @@
using System;
namespace CryptoExchange.Net.Attributes
{
/// <summary>
/// Marks property as optional
/// </summary>
public class JsonOptionalPropertyAttribute : Attribute
{
}
}

View File

@ -7,7 +7,7 @@ using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net.Authentication
{
/// <summary>
/// Api credentials info
/// Api credentials, used to sign requests accessing private endpoints
/// </summary>
public class ApiCredentials: IDisposable
{

View File

@ -30,7 +30,7 @@ namespace CryptoExchange.Net
/// </summary>
protected internal Log log;
/// <summary>
/// The authentication provider
/// The authentication provider when api credentials have been provided
/// </summary>
protected internal AuthenticationProvider? authProvider;
/// <summary>
@ -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
}
/// <summary>
/// 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
/// </summary>
/// <param name="data">The data to parse</param>
/// <returns></returns>

View File

@ -181,6 +181,7 @@ namespace CryptoExchange.Net.Converters
/// <summary>
/// Mark property as an index in the array
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ArrayPropertyAttribute: Attribute
{
/// <summary>

View File

@ -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<T, string>)))
mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));

View File

@ -4,7 +4,7 @@ using Newtonsoft.Json;
namespace CryptoExchange.Net.Converters
{
/// <summary>
/// converter for milliseconds to datetime
/// Converter for milliseconds to datetime
/// </summary>
public class TimestampConverter : JsonConverter
{

View File

@ -18,7 +18,7 @@ namespace CryptoExchange.Net.Interfaces
IRequestFactory RequestFactory { get; set; }
/// <summary>
/// The total amount of requests made
/// The total amount of requests made with this client
/// </summary>
int TotalRequestsMade { get; }
@ -34,7 +34,7 @@ namespace CryptoExchange.Net.Interfaces
void RemoveRateLimiters();
/// <summary>
/// Client options
/// The options provided for this client
/// </summary>
RestClientOptions ClientOptions { get; }
}

View File

@ -11,7 +11,7 @@ namespace CryptoExchange.Net.Interfaces
public interface ISocketClient: IDisposable
{
/// <summary>
/// Client options
/// The options provided for this client
/// </summary>
SocketClientOptions ClientOptions { get; }
@ -20,6 +20,13 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
public double IncomingKbps { get; }
/// <summary>
/// Unsubscribe from a stream using the subscription id received when starting the subscription
/// </summary>
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
/// <returns></returns>
Task UnsubscribeAsync(int subscriptionId);
/// <summary>
/// Unsubscribe from a stream
/// </summary>

View File

@ -7,41 +7,41 @@ using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Interface for websocket interaction
/// Webscoket connection interface
/// </summary>
public interface IWebsocket: IDisposable
{
/// <summary>
/// Websocket closed
/// Websocket closed event
/// </summary>
event Action OnClose;
/// <summary>
/// Websocket message received
/// Websocket message received event
/// </summary>
event Action<string> OnMessage;
/// <summary>
/// Websocket error
/// Websocket error event
/// </summary>
event Action<Exception> OnError;
/// <summary>
/// Websocket opened
/// Websocket opened event
/// </summary>
event Action OnOpen;
/// <summary>
/// Id
/// Unique id for this socket
/// </summary>
int Id { get; }
/// <summary>
/// Origin
/// Origin header
/// </summary>
string? Origin { get; set; }
/// <summary>
/// Encoding to use
/// Encoding to use for sending/receiving string data
/// </summary>
Encoding? Encoding { get; set; }
/// <summary>
/// Reconnecting
/// Whether socket is in the process of reconnecting
/// </summary>
bool Reconnecting { get; set; }
/// <summary>
@ -61,15 +61,15 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
Func<string, string>? DataInterpreterString { get; set; }
/// <summary>
/// Socket url
/// The url the socket connects to
/// </summary>
string Url { get; }
/// <summary>
/// Is closed
/// Whether the socket connection is closed
/// </summary>
bool IsClosed { get; }
/// <summary>
/// Is open
/// Whether the socket connection is open
/// </summary>
bool IsOpen { get; }
/// <summary>
@ -77,10 +77,15 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
SslProtocols SSLProtocols { get; set; }
/// <summary>
/// Timeout
/// The max time for no data being received before the connection is considered lost
/// </summary>
TimeSpan Timeout { get; set; }
/// <summary>
/// Set a proxy to use when connecting
/// </summary>
/// <param name="proxy"></param>
void SetProxy(ApiProxy proxy);
/// <summary>
/// Connect the socket
/// </summary>
/// <returns></returns>
@ -91,18 +96,13 @@ namespace CryptoExchange.Net.Interfaces
/// <param name="data"></param>
void Send(string data);
/// <summary>
/// Reset socket
/// Reset socket when a connection is lost to prepare for a new connection
/// </summary>
void Reset();
/// <summary>
/// Close the connecting
/// Close the connection
/// </summary>
/// <returns></returns>
Task CloseAsync();
/// <summary>
/// Set proxy
/// </summary>
/// <param name="proxy"></param>
void SetProxy(ApiProxy proxy);
}
}

View File

@ -11,17 +11,17 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Create a websocket for an url
/// </summary>
/// <param name="log"></param>
/// <param name="url"></param>
/// <param name="log">The logger</param>
/// <param name="url">The url the socket is fo</param>
/// <returns></returns>
IWebsocket CreateWebsocket(Log log, string url);
/// <summary>
/// Create a websocket for an url
/// </summary>
/// <param name="log"></param>
/// <param name="url"></param>
/// <param name="cookies"></param>
/// <param name="headers"></param>
/// <param name="log">The logger</param>
/// <param name="url">The url the socket is fo</param>
/// <param name="cookies">Cookies to be send in the initial request</param>
/// <param name="headers">Headers to be send in the initial request</param>
/// <returns></returns>
IWebsocket CreateWebsocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers);
}

View File

@ -4,7 +4,7 @@ using System;
namespace CryptoExchange.Net.Logging
{
/// <summary>
/// Log to console
/// ILogger implementation for logging to the console
/// </summary>
public class ConsoleLogger : ILogger
{

View File

@ -5,7 +5,7 @@ using System.Diagnostics;
namespace CryptoExchange.Net.Logging
{
/// <summary>
/// Default log writer, writes to debug
/// Default log writer, uses Trace.WriteLine
/// </summary>
public class DebugLogger: ILogger
{

View File

@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Objects
public class BaseOptions
{
/// <summary>
/// The minimum log level to output. Setting it to null will send all messages to the registered ILoggers.
/// The minimum log level to output
/// </summary>
public LogLevel LogLevel { get; set; } = LogLevel.Information;
@ -86,12 +86,12 @@ namespace CryptoExchange.Net.Objects
}
/// <summary>
/// The api credentials
/// The api credentials used for signing requests
/// </summary>
public ApiCredentials? ApiCredentials { get; set; }
/// <summary>
/// Proxy to use
/// Proxy to use when connecting
/// </summary>
public ApiProxy? Proxy { get; set; }
@ -161,7 +161,7 @@ namespace CryptoExchange.Net.Objects
/// <inheritdoc />
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);
/// <summary>
/// The maximum number of times to try to reconnect
/// The maximum number of times to try to reconnect, default null will retry indefinitely
/// </summary>
public int? MaxReconnectTries { get; set; }
@ -196,11 +196,13 @@ namespace CryptoExchange.Net.Objects
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
/// <summary>
/// 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
/// </summary>
public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10);
/// <summary>
/// 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
/// </summary>
public TimeSpan SocketNoDataTimeout { get; set; }
@ -234,7 +236,7 @@ namespace CryptoExchange.Net.Objects
/// <inheritdoc />
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}";
}
}
}

View File

@ -10,19 +10,19 @@ namespace CryptoExchange.Net.OrderBook
public class ProcessBufferRangeSequenceEntry
{
/// <summary>
/// First update id
/// First sequence number in this update
/// </summary>
public long FirstUpdateId { get; set; }
/// <summary>
/// Last update id
/// Last sequence number in this update
/// </summary>
public long LastUpdateId { get; set; }
/// <summary>
/// List of asks
/// List of changed/new asks
/// </summary>
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
/// <summary>
/// List of bids
/// List of changed/new bids
/// </summary>
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
}

View File

@ -18,52 +18,64 @@ namespace CryptoExchange.Net.OrderBook
/// </summary>
public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable
{
/// <summary>
/// The process buffer, used while syncing
/// </summary>
protected readonly List<ProcessBufferRangeSequenceEntry> processBuffer;
/// <summary>
/// The ask list
/// </summary>
protected SortedList<decimal, ISymbolOrderBookEntry> asks;
/// <summary>
/// The bid list
/// </summary>
protected SortedList<decimal, ISymbolOrderBookEntry> 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<object> _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();
/// <summary>
/// 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
/// </summary>
public string Id { get; }
protected readonly List<ProcessBufferRangeSequenceEntry> processBuffer;
/// <summary>
/// The ask list, should only be accessed using the bookLock
/// </summary>
protected SortedList<decimal, ISymbolOrderBookEntry> asks;
/// <summary>
/// The bid list, should only be accessed using the bookLock
/// </summary>
protected SortedList<decimal, ISymbolOrderBookEntry> bids;
/// <summary>
/// The log
/// </summary>
protected Log log;
/// <summary>
/// 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
/// </summary>
protected bool sequencesAreConsecutive;
/// <summary>
/// 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
/// </summary>
protected bool strictLevels;
/// <summary>
/// If order book is set
/// If the initial snapshot of the book has been set
/// </summary>
protected bool bookSet;
@ -72,9 +84,10 @@ namespace CryptoExchange.Net.OrderBook
/// </summary>
protected int? Levels { get; set; } = null;
/// <summary>
/// The status of the order book. Order book is up to date when the status is `Synced`
/// </summary>
/// <inheritdoc/>
public string Id { get; }
/// <inheritdoc/>
public OrderBookStatus Status
{
get => status;
@ -90,46 +103,31 @@ namespace CryptoExchange.Net.OrderBook
}
}
/// <summary>
/// Last update identifier
/// </summary>
/// <inheritdoc/>
public long LastSequenceNumber { get; private set; }
/// <summary>
/// The symbol of the order book
/// </summary>
/// <inheritdoc/>
public string Symbol { get; }
/// <summary>
/// Event when the state changes
/// </summary>
/// <inheritdoc/>
public event Action<OrderBookStatus, OrderBookStatus>? OnStatusChange;
/// <summary>
/// Event when the BestBid or BestAsk changes ie a Pricing Tick
/// </summary>
/// <inheritdoc/>
public event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)>? OnBestOffersChanged;
/// <summary>
/// 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
/// </summary>
/// <inheritdoc/>
public event Action<(IEnumerable<ISymbolOrderBookEntry> Bids, IEnumerable<ISymbolOrderBookEntry> Asks)>? OnOrderBookUpdate;
/// <summary>
/// Timestamp of the last update
/// </summary>
/// <inheritdoc/>
public DateTime UpdateTime { get; private set; }
/// <summary>
/// The number of asks in the book
/// </summary>
/// <inheritdoc/>
public int AskCount { get; private set; }
/// <summary>
/// The number of bids in the book
/// </summary>
/// <inheritdoc/>
public int BidCount { get; private set; }
/// <summary>
/// The list of asks
/// </summary>
/// <inheritdoc/>
public IEnumerable<ISymbolOrderBookEntry> Asks
{
get
@ -139,9 +137,7 @@ namespace CryptoExchange.Net.OrderBook
}
}
/// <summary>
/// The list of bids
/// </summary>
/// <inheritdoc/>
public IEnumerable<ISymbolOrderBookEntry> Bids
{
get
@ -151,9 +147,7 @@ namespace CryptoExchange.Net.OrderBook
}
}
/// <summary>
/// Get a snapshot of the book at this moment
/// </summary>
/// <inheritdoc/>
public (IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> 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();
/// <summary>
/// The best bid currently in the order book
/// </summary>
/// <inheritdoc/>
public ISymbolOrderBookEntry BestBid
{
get
@ -183,9 +167,7 @@ namespace CryptoExchange.Net.OrderBook
}
}
/// <summary>
/// The best ask currently in the order book
/// </summary>
/// <inheritdoc/>
public ISymbolOrderBookEntry BestAsk
{
get
@ -195,9 +177,7 @@ namespace CryptoExchange.Net.OrderBook
}
}
/// <summary>
/// BestBid/BesAsk returned as a pair
/// </summary>
/// <inheritdoc/>
public (ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers {
get {
lock (bookLock)
@ -208,9 +188,9 @@ namespace CryptoExchange.Net.OrderBook
/// <summary>
/// ctor
/// </summary>
/// <param name="id"></param>
/// <param name="symbol"></param>
/// <param name="options"></param>
/// <param name="id">The id of the order book. Should be set to {Exchange}[{type}], for example: Kucoin[Spot]</param>
/// <param name="symbol">The symbol the order book is for</param>
/// <param name="options">The options for the order book</param>
protected SymbolOrderBook(string id, string symbol, OrderBookOptions options)
{
if (symbol == null)
@ -236,10 +216,7 @@ namespace CryptoExchange.Net.OrderBook
log.UpdateWriters(writers.ToList());
}
/// <summary>
/// Start connecting and synchronizing the order book
/// </summary>
/// <returns></returns>
/// <inheritdoc/>
public async Task<CallResult<bool>> StartAsync()
{
if (Status != OrderBookStatus.Disconnected)
@ -275,13 +252,21 @@ namespace CryptoExchange.Net.OrderBook
return new CallResult<bool>(true, null);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="quantity">The quantity in base asset to fill</param>
/// <param name="type">The type</param>
/// <returns>Average fill price</returns>
/// <inheritdoc/>
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");
}
/// <inheritdoc/>
public CallResult<decimal> CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type)
{
if (Status != OrderBookStatus.Synced)
@ -312,6 +297,219 @@ namespace CryptoExchange.Net.OrderBook
return new CallResult<decimal>(Math.Round(totalCost / totalAmount, 8), null);
}
/// <summary>
/// Implementation for starting the order book. Should typically have logic for subscribing to the update stream and retrieving
/// and setting the initial order book
/// </summary>
/// <returns></returns>
protected abstract Task<CallResult<UpdateSubscription>> DoStartAsync();
/// <summary>
/// Reset the order book
/// </summary>
protected virtual void DoReset() { }
/// <summary>
/// Resync the order book
/// </summary>
/// <returns></returns>
protected abstract Task<CallResult<bool>> DoResyncAsync();
/// <summary>
/// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false)
/// the order book will be resynchronized
/// </summary>
/// <param name="checksum"></param>
/// <returns></returns>
protected virtual bool DoChecksum(int checksum) => true;
/// <summary>
/// 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
/// </summary>
/// <param name="orderBookSequenceNumber">The last update sequence number until which the snapshot is in sync</param>
/// <param name="askList">List of asks</param>
/// <param name="bidList">List of bids</param>
protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable<ISymbolOrderBookEntry> bidList, IEnumerable<ISymbolOrderBookEntry> askList)
{
_processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList });
_queueEvent.Set();
}
/// <summary>
/// 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
/// </summary>
/// <param name="updateId">The sequence number</param>
/// <param name="bids">List of updated/new bids</param>
/// <param name="asks">List of updated/new asks</param>
protected void UpdateOrderBook(long updateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = updateId, EndUpdateId = updateId, Asks = asks, Bids = bids });
_queueEvent.Set();
}
/// <summary>
/// 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
/// </summary>
/// <param name="firstUpdateId">The sequence number of the first update</param>
/// <param name="lastUpdateId">The sequence number of the last update</param>
/// <param name="bids">List of updated/new bids</param>
/// <param name="asks">List of updated/new asks</param>
protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids });
_queueEvent.Set();
}
/// <summary>
/// Add an update to the process queue. Updates the book by providing changed bids and asks, each with its own sequence number
/// </summary>
/// <param name="bids">List of updated/new bids</param>
/// <param name="asks">List of updated/new asks</param>
protected void UpdateOrderBook(IEnumerable<ISymbolOrderSequencedBookEntry> bids, IEnumerable<ISymbolOrderSequencedBookEntry> 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();
}
/// <summary>
/// Add a checksum value to the process queue
/// </summary>
/// <param name="checksum">The checksum value</param>
protected void AddChecksum(int checksum)
{
_processQueue.Enqueue(new ChecksumItem() { Checksum = checksum });
_queueEvent.Set();
}
/// <summary>
/// Check and empty the process buffer; see what entries to update the book with
/// </summary>
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);
}
}
/// <summary>
/// Update order book with an entry
/// </summary>
/// <param name="sequence">Sequence number of the update</param>
/// <param name="type">Type of entry</param>
/// <param name="entry">The entry</param>
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;
}
/// <summary>
/// Wait until the order book snapshot has been set
/// </summary>
/// <param name="timeout">Max wait time</param>
/// <returns></returns>
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(int timeout)
{
var startWait = DateTime.UtcNow;
while (!bookSet && Status == OrderBookStatus.Syncing)
{
if ((DateTime.UtcNow - startWait).TotalMilliseconds > timeout)
return new CallResult<bool>(false, new ServerError("Timeout while waiting for data"));
await Task.Delay(10).ConfigureAwait(false);
}
return new CallResult<bool>(true, null);
}
/// <summary>
/// Dispose the order book
/// </summary>
public abstract void Dispose();
/// <summary>
/// String representation of the top 3 entries
/// </summary>
/// <returns></returns>
public override string ToString()
{
return ToString(3);
}
/// <summary>
/// String representation of the top x entries
/// </summary>
/// <returns></returns>
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;
}
/// <summary>
/// Stop syncing the order book
/// </summary>
/// <returns></returns>
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");
}
/// <summary>
/// Start the order book
/// </summary>
/// <returns></returns>
protected abstract Task<CallResult<UpdateSubscription>> DoStartAsync();
/// <summary>
/// Reset the order book
/// </summary>
protected virtual void DoReset() { }
/// <summary>
/// Resync the order book
/// </summary>
/// <returns></returns>
protected abstract Task<CallResult<bool>> DoResyncAsync();
/// <summary>
/// Validate a checksum with the current order book
/// </summary>
/// <param name="checksum"></param>
/// <returns></returns>
protected virtual bool DoChecksum(int checksum) => true;
private void ProcessQueue()
{
while(Status != OrderBookStatus.Disconnected)
while (Status != OrderBookStatus.Disconnected)
{
_queueEvent.WaitOne();
@ -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
});
}
/// <summary>
/// Set the initial data for the order book
/// </summary>
/// <param name="orderBookSequenceNumber">The last update sequence number</param>
/// <param name="askList">List of asks</param>
/// <param name="bidList">List of bids</param>
protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable<ISymbolOrderBookEntry> bidList, IEnumerable<ISymbolOrderBookEntry> askList)
{
_processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList });
_queueEvent.Set();
}
/// <summary>
/// Update the order book using a single id for an update
/// </summary>
/// <param name="rangeUpdateId"></param>
/// <param name="bids"></param>
/// <param name="asks"></param>
protected void UpdateOrderBook(long rangeUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = rangeUpdateId, EndUpdateId = rangeUpdateId, Asks = asks, Bids = bids });
_queueEvent.Set();
}
/// <summary>
/// Add a checksum to the process queue
/// </summary>
/// <param name="checksum"></param>
protected void AddChecksum(int checksum)
{
_processQueue.Enqueue(new ChecksumItem() { Checksum = checksum });
_queueEvent.Set();
}
/// <summary>
/// Update the order book using a first/last update id
/// </summary>
/// <param name="firstUpdateId"></param>
/// <param name="lastUpdateId"></param>
/// <param name="bids"></param>
/// <param name="asks"></param>
protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids });
_queueEvent.Set();
}
/// <summary>
/// Update the order book using sequenced entries
/// </summary>
/// <param name="bids">List of bids</param>
/// <param name="asks">List of asks</param>
protected void UpdateOrderBook(IEnumerable<ISymbolOrderSequencedBookEntry> bids, IEnumerable<ISymbolOrderSequencedBookEntry> 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<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
if (lastUpdateId <= LastSequenceNumber)
@ -613,131 +709,6 @@ namespace CryptoExchange.Net.OrderBook
LastSequenceNumber = lastUpdateId;
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
}
/// <summary>
/// Check and empty the process buffer; see what entries to update the book with
/// </summary>
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);
}
}
/// <summary>
/// Update order book with an entry
/// </summary>
/// <param name="sequence">Sequence number of the update</param>
/// <param name="type">Type of entry</param>
/// <param name="entry">The entry</param>
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;
}
/// <summary>
/// Wait until the order book has been set
/// </summary>
/// <param name="timeout">Max wait time</param>
/// <returns></returns>
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(int timeout)
{
var startWait = DateTime.UtcNow;
while (!bookSet && Status == OrderBookStatus.Syncing)
{
if ((DateTime.UtcNow - startWait).TotalMilliseconds > timeout)
return new CallResult<bool>(false, new ServerError("Timeout while waiting for data"));
await Task.Delay(10).ConfigureAwait(false);
}
return new CallResult<bool>(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));
}
/// <summary>
/// Dispose the order book
/// </summary>
public abstract void Dispose();
/// <summary>
/// String representation of the top 3 entries
/// </summary>
/// <returns></returns>
public override string ToString()
{
return ToString(3);
}
/// <summary>
/// String representation of the top x entries
/// </summary>
/// <returns></returns>
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<T> : IComparer<T>

View File

@ -11,7 +11,7 @@ using CryptoExchange.Net.Interfaces;
namespace CryptoExchange.Net.Requests
{
/// <summary>
/// Request object
/// Request object, wrapper for HttpRequestMessage
/// </summary>
public class Request : IRequest
{
@ -49,6 +49,7 @@ namespace CryptoExchange.Net.Requests
/// <inheritdoc />
public Uri Uri => request.RequestUri;
/// <inheritdoc />
public int RequestId { get; }

View File

@ -7,7 +7,7 @@ using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.Requests
{
/// <summary>
/// WebRequest factory
/// Request factory
/// </summary>
public class RequestFactory : IRequestFactory
{

View File

@ -8,7 +8,7 @@ using CryptoExchange.Net.Interfaces;
namespace CryptoExchange.Net.Requests
{
/// <summary>
/// HttpWebResponse response object
/// Response object, wrapper for HttpResponseMessage
/// </summary>
internal class Response : IResponse
{

View File

@ -67,9 +67,7 @@ namespace CryptoExchange.Net
/// </summary>
protected IEnumerable<IRateLimiter> RateLimiters { get; private set; }
/// <summary>
/// Total requests made by this client
/// </summary>
/// <inheritdoc />
public int TotalRequestsMade { get; private set; }
/// <summary>
@ -133,7 +131,6 @@ namespace CryptoExchange.Net
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="checkResult">Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug)</param>
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
/// <param name="credits">Credits used for the request</param>
@ -147,7 +144,6 @@ namespace CryptoExchange.Net
CancellationToken cancellationToken,
Dictionary<string, object>? parameters = null,
bool signed = false,
bool checkResult = true,
HttpMethodParameterPosition? parameterPosition = null,
ArrayParametersSerialization? arraySerialization = null,
int credits = 1,

View File

@ -80,9 +80,7 @@ namespace CryptoExchange.Net
/// </summary>
protected internal int? RateLimitPerSocketPerSecond { get; set; }
/// <summary>
/// The current kilobytes per second of data being received by all connection from this client, averaged over the last 3 seconds
/// </summary>
/// <inheritdoc />
public double IncomingKbps
{
get

View File

@ -42,7 +42,7 @@ namespace CryptoExchange.Net.Sockets
private DateTime _lastReceivedMessagesUpdate;
/// <summary>
/// Received messages time -> size
/// Received messages, the size and the timstamp
/// </summary>
protected readonly List<ReceiveItem> _receivedMessages;
/// <summary>
@ -72,17 +72,15 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
protected readonly List<Action<string>> messageHandlers = new List<Action<string>>();
/// <summary>
/// The id of this socket
/// </summary>
/// <inheritdoc />
public int Id { get; }
/// <inheritdoc />
public string? Origin { get; set; }
/// <summary>
/// Whether this socket is currently reconnecting
/// </summary>
/// <inheritdoc />
public bool Reconnecting { get; set; }
/// <summary>
/// The timestamp this socket has been active for the last time
/// </summary>
@ -92,22 +90,19 @@ namespace CryptoExchange.Net.Sockets
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
/// </summary>
public Func<byte[], string>? DataInterpreterBytes { get; set; }
/// <summary>
/// Delegate used for processing string data received from socket connections before it is processed by handlers
/// </summary>
public Func<string, string>? DataInterpreterString { get; set; }
/// <summary>
/// Url this socket connects to
/// </summary>
/// <inheritdoc />
public string Url { get; }
/// <summary>
/// If the connection is closed
/// </summary>
/// <inheritdoc />
public bool IsClosed => _socket.State == WebSocketState.Closed;
/// <summary>
/// If the connection is open
/// </summary>
/// <inheritdoc />
public bool IsOpen => _socket.State == WebSocketState.Open && !_closing;
/// <summary>
@ -116,9 +111,7 @@ namespace CryptoExchange.Net.Sockets
public SslProtocols SSLProtocols { get; set; }
private Encoding _encoding = Encoding.UTF8;
/// <summary>
/// Encoding used for decoding the received bytes into a string
/// </summary>
/// <inheritdoc />
public Encoding? Encoding
{
get => _encoding;
@ -128,19 +121,16 @@ namespace CryptoExchange.Net.Sockets
_encoding = value;
}
}
/// <summary>
/// The max amount of outgoing messages per second
/// </summary>
public int? RatelimitPerSecond { get; set; }
/// <summary>
/// The timespan no data is received on the socket. If no data is received within this time an error is generated
/// </summary>
/// <inheritdoc />
public TimeSpan Timeout { get; set; }
/// <summary>
/// The current kilobytes per second of data being received, averaged over the last 3 seconds
/// </summary>
/// <inheritdoc />
public double IncomingKbps
{
get
@ -157,33 +147,28 @@ namespace CryptoExchange.Net.Sockets
}
}
/// <summary>
/// Socket closed event
/// </summary>
/// <inheritdoc />
public event Action OnClose
{
add => closeHandlers.Add(value);
remove => closeHandlers.Remove(value);
}
/// <summary>
/// Socket message received event
/// </summary>
/// <inheritdoc />
public event Action<string> OnMessage
{
add => messageHandlers.Add(value);
remove => messageHandlers.Remove(value);
}
/// <summary>
/// Socket error event
/// </summary>
/// <inheritdoc />
public event Action<Exception> OnError
{
add => errorHandlers.Add(value);
remove => errorHandlers.Remove(value);
}
/// <summary>
/// Socket opened event
/// </summary>
/// <inheritdoc />
public event Action OnOpen
{
add => openHandlers.Add(value);
@ -224,10 +209,7 @@ namespace CryptoExchange.Net.Sockets
_socket = CreateSocket();
}
/// <summary>
/// Set a proxy to use. Should be set before connecting
/// </summary>
/// <param name="proxy"></param>
/// <inheritdoc />
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);
}
/// <summary>
/// Connect the websocket
/// </summary>
/// <returns>True if successfull</returns>
/// <inheritdoc />
public virtual async Task<bool> ConnectAsync()
{
log.Write(LogLevel.Debug, $"Socket {Id} connecting");
@ -270,10 +249,7 @@ namespace CryptoExchange.Net.Sockets
return true;
}
/// <summary>
/// Send data over the websocket
/// </summary>
/// <param name="data">Data to send</param>
/// <inheritdoc />
public virtual void Send(string data)
{
if (_closing)
@ -285,10 +261,7 @@ namespace CryptoExchange.Net.Sockets
_sendEvent.Set();
}
/// <summary>
/// Close the websocket
/// </summary>
/// <returns></returns>
/// <inheritdoc />
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");
}
/// <summary>
/// Reset the socket so a new connection can be attempted after it has been connected before
/// </summary>
/// <inheritdoc />
public void Reset()
{
log.Write(LogLevel.Debug, $"Socket {Id} resetting");

View File

@ -26,7 +26,7 @@ namespace CryptoExchange.Net.Sockets
public DateTime ReceivedTimestamp { get; set; }
/// <summary>
///
/// ctor
/// </summary>
/// <param name="connection"></param>
/// <param name="jsonData"></param>

View File

@ -14,7 +14,7 @@ using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Socket connecting
/// A single socket connection to the server
/// </summary>
public class SocketConnection
{
@ -22,26 +22,32 @@ namespace CryptoExchange.Net.Sockets
/// Connection lost event
/// </summary>
public event Action? ConnectionLost;
/// <summary>
/// Connection closed and no reconnect is happening
/// </summary>
public event Action? ConnectionClosed;
/// <summary>
/// Connecting restored event
/// </summary>
public event Action<TimeSpan>? ConnectionRestored;
/// <summary>
/// The connection is paused event
/// </summary>
public event Action? ActivityPaused;
/// <summary>
/// The connection is unpaused event
/// </summary>
public event Action? ActivityUnpaused;
/// <summary>
/// Connecting closed event
/// </summary>
public event Action? Closed;
/// <summary>
/// Unhandled message event
/// </summary>
@ -57,30 +63,35 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// If connection is authenticated
/// If the connection has been authenticated
/// </summary>
public bool Authenticated { get; set; }
/// <summary>
/// If connection is made
/// </summary>
public bool Connected { get; private set; }
/// <summary>
/// The underlying socket
/// The underlying websocket
/// </summary>
public IWebsocket Socket { get; set; }
/// <summary>
/// If the socket should be reconnected upon closing
/// </summary>
public bool ShouldReconnect { get; set; }
/// <summary>
/// Current reconnect try
/// Current reconnect try, reset when a successful connection is made
/// </summary>
public int ReconnectTry { get; set; }
/// <summary>
/// Current resubscribe try
/// Current resubscribe try, reset when a successful connection is made
/// </summary>
public int ResubscribeTry { get; set; }
/// <summary>
/// Time of disconnecting
/// </summary>
@ -138,7 +149,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Process a message received by the socket
/// </summary>
/// <param name="data"></param>
/// <param name="data">The received data</param>
private void ProcessMessage(string data)
{
var timestamp = DateTime.UtcNow;
@ -193,7 +204,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Add subscription to this connection
/// Add a subscription to this connection
/// </summary>
/// <param name="subscription"></param>
public void AddSubscription(SocketSubscription subscription)
@ -203,15 +214,20 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Get a subscription on this connection
/// Get a subscription on this connection by id
/// </summary>
/// <param name="id"></param>
public SocketSubscription GetSubscription(int id)
public SocketSubscription? GetSubscription(int id)
{
lock (subscriptionLock)
return subscriptions.SingleOrDefault(s => s.Id == id);
}
/// <summary>
/// Process data
/// </summary>
/// <param name="messageEvent"></param>
/// <returns>True if the data was successfully handled</returns>
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
/// <typeparam name="T">The data type expected in response</typeparam>
/// <param name="obj">The object to send</param>
/// <param name="timeout">The timeout for response</param>
/// <param name="handler">The response handler</param>
/// <param name="handler">The response handler, should return true if the received JToken was the response to the request</param>
/// <returns></returns>
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, Func<JToken, bool> 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<bool> ProcessReconnectAsync()
{
if (Authenticated)

View File

@ -9,9 +9,10 @@ namespace CryptoExchange.Net.Sockets
public class SocketSubscription
{
/// <summary>
/// Subscription id
/// Unique subscription id
/// </summary>
public int Id { get; }
/// <summary>
/// Exception event
/// </summary>
@ -23,25 +24,28 @@ namespace CryptoExchange.Net.Sockets
public Action<MessageEvent> MessageHandler { get; set; }
/// <summary>
/// Request object
/// The request object send when subscribing on the server. Either this or the `Identifier` property should be set
/// </summary>
public object? Request { get; set; }
/// <summary>
/// Subscription identifier
/// The subscription identifier, used instead of a `Request` object to identify the subscription
/// </summary>
public string? Identifier { get; set; }
/// <summary>
/// Is user subscription or generic
/// Whether this is a user subscription or an internal listener
/// </summary>
public bool UserSubscription { get; set; }
/// <summary>
/// If the subscription has been confirmed
/// If the subscription has been confirmed to be subscribed by the server
/// </summary>
public bool Confirmed { get; set; }
/// <summary>
/// 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
/// </summary>
public CancellationTokenRegistration? CancellationTokenRegistration { get; set; }
@ -55,7 +59,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Create SocketSubscription for a request
/// Create SocketSubscription for a subscribe request
/// </summary>
/// <param name="id"></param>
/// <param name="request"></param>

View File

@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Event when the connection is closed. This event happens when reconnecting/resubscribing has failed too often based on the <see cref="SocketClientOptions.MaxReconnectTries"/> and <see cref="SocketClientOptions.MaxResubscribeTries"/> options,
/// or <see cref="SocketClientOptions.AutoReconnect"/> is false
/// or <see cref="SocketClientOptions.AutoReconnect"/> is false. The socket will not be reconnected
/// </summary>
public event Action ConnectionClosed
{
@ -33,8 +33,8 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// 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.
/// </summary>
public event Action<TimeSpan> ConnectionRestored
{