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
Assert.IsTrue(client.ClientOptions.BaseAddress == "http://test.address.com/"); 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.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
Assert.IsTrue(client.ClientOptions.RequestTimeout == TimeSpan.FromMinutes(1)); Assert.IsTrue(client.ClientOptions.RequestTimeout == TimeSpan.FromMinutes(1));
} }

View File

@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes
/// <summary> /// <summary>
/// Used for conversion in ArrayConverter /// Used for conversion in ArrayConverter
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class JsonConversionAttribute: Attribute 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 namespace CryptoExchange.Net.Authentication
{ {
/// <summary> /// <summary>
/// Api credentials info /// Api credentials, used to sign requests accessing private endpoints
/// </summary> /// </summary>
public class ApiCredentials: IDisposable public class ApiCredentials: IDisposable
{ {

View File

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

View File

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

View File

@ -75,7 +75,7 @@ namespace CryptoExchange.Net.Converters
private bool GetValue(string value, out T result) 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)); var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
if(mapping.Equals(default(KeyValuePair<T, string>))) if(mapping.Equals(default(KeyValuePair<T, string>)))
mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));

View File

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

View File

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

View File

@ -11,7 +11,7 @@ namespace CryptoExchange.Net.Interfaces
public interface ISocketClient: IDisposable public interface ISocketClient: IDisposable
{ {
/// <summary> /// <summary>
/// Client options /// The options provided for this client
/// </summary> /// </summary>
SocketClientOptions ClientOptions { get; } SocketClientOptions ClientOptions { get; }
@ -20,6 +20,13 @@ namespace CryptoExchange.Net.Interfaces
/// </summary> /// </summary>
public double IncomingKbps { get; } 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> /// <summary>
/// Unsubscribe from a stream /// Unsubscribe from a stream
/// </summary> /// </summary>

View File

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

View File

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

View File

@ -15,7 +15,7 @@ namespace CryptoExchange.Net.Objects
public class BaseOptions public class BaseOptions
{ {
/// <summary> /// <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> /// </summary>
public LogLevel LogLevel { get; set; } = LogLevel.Information; public LogLevel LogLevel { get; set; } = LogLevel.Information;
@ -86,12 +86,12 @@ namespace CryptoExchange.Net.Objects
} }
/// <summary> /// <summary>
/// The api credentials /// The api credentials used for signing requests
/// </summary> /// </summary>
public ApiCredentials? ApiCredentials { get; set; } public ApiCredentials? ApiCredentials { get; set; }
/// <summary> /// <summary>
/// Proxy to use /// Proxy to use when connecting
/// </summary> /// </summary>
public ApiProxy? Proxy { get; set; } public ApiProxy? Proxy { get; set; }
@ -161,7 +161,7 @@ namespace CryptoExchange.Net.Objects
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() 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); public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
/// <summary> /// <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> /// </summary>
public int? MaxReconnectTries { get; set; } public int? MaxReconnectTries { get; set; }
@ -196,11 +196,13 @@ namespace CryptoExchange.Net.Objects
public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5; public int MaxConcurrentResubscriptionsPerSocket { get; set; } = 5;
/// <summary> /// <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> /// </summary>
public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10); public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10);
/// <summary> /// <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> /// </summary>
public TimeSpan SocketNoDataTimeout { get; set; } public TimeSpan SocketNoDataTimeout { get; set; }
@ -234,7 +236,7 @@ namespace CryptoExchange.Net.Objects
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() 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 public class ProcessBufferRangeSequenceEntry
{ {
/// <summary> /// <summary>
/// First update id /// First sequence number in this update
/// </summary> /// </summary>
public long FirstUpdateId { get; set; } public long FirstUpdateId { get; set; }
/// <summary> /// <summary>
/// Last update id /// Last sequence number in this update
/// </summary> /// </summary>
public long LastUpdateId { get; set; } public long LastUpdateId { get; set; }
/// <summary> /// <summary>
/// List of asks /// List of changed/new asks
/// </summary> /// </summary>
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>(); public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
/// <summary> /// <summary>
/// List of bids /// List of changed/new bids
/// </summary> /// </summary>
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>(); public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
} }

View File

@ -18,52 +18,64 @@ namespace CryptoExchange.Net.OrderBook
/// </summary> /// </summary>
public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable 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 readonly object bookLock = new object();
private OrderBookStatus status; private OrderBookStatus status;
private UpdateSubscription? subscription; private UpdateSubscription? subscription;
private readonly bool validateChecksum;
private bool _stopProcessing; private bool _stopProcessing;
private Task? _processTask; private Task? _processTask;
private readonly AutoResetEvent _queueEvent; private readonly AutoResetEvent _queueEvent;
private readonly ConcurrentQueue<object> _processQueue; 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> /// <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> /// </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> /// <summary>
/// The log /// The log
/// </summary> /// </summary>
protected Log log; protected Log log;
/// <summary> /// <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> /// </summary>
protected bool sequencesAreConsecutive; protected bool sequencesAreConsecutive;
/// <summary> /// <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> /// </summary>
protected bool strictLevels; protected bool strictLevels;
/// <summary> /// <summary>
/// If order book is set /// If the initial snapshot of the book has been set
/// </summary> /// </summary>
protected bool bookSet; protected bool bookSet;
@ -72,9 +84,10 @@ namespace CryptoExchange.Net.OrderBook
/// </summary> /// </summary>
protected int? Levels { get; set; } = null; protected int? Levels { get; set; } = null;
/// <summary> /// <inheritdoc/>
/// The status of the order book. Order book is up to date when the status is `Synced` public string Id { get; }
/// </summary>
/// <inheritdoc/>
public OrderBookStatus Status public OrderBookStatus Status
{ {
get => status; get => status;
@ -90,46 +103,31 @@ namespace CryptoExchange.Net.OrderBook
} }
} }
/// <summary> /// <inheritdoc/>
/// Last update identifier
/// </summary>
public long LastSequenceNumber { get; private set; } public long LastSequenceNumber { get; private set; }
/// <summary>
/// The symbol of the order book /// <inheritdoc/>
/// </summary>
public string Symbol { get; } public string Symbol { get; }
/// <summary> /// <inheritdoc/>
/// Event when the state changes
/// </summary>
public event Action<OrderBookStatus, OrderBookStatus>? OnStatusChange; public event Action<OrderBookStatus, OrderBookStatus>? OnStatusChange;
/// <summary> /// <inheritdoc/>
/// Event when the BestBid or BestAsk changes ie a Pricing Tick
/// </summary>
public event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)>? OnBestOffersChanged; public event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)>? OnBestOffersChanged;
/// <summary> /// <inheritdoc/>
/// 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>
public event Action<(IEnumerable<ISymbolOrderBookEntry> Bids, IEnumerable<ISymbolOrderBookEntry> Asks)>? OnOrderBookUpdate; public event Action<(IEnumerable<ISymbolOrderBookEntry> Bids, IEnumerable<ISymbolOrderBookEntry> Asks)>? OnOrderBookUpdate;
/// <summary>
/// Timestamp of the last update /// <inheritdoc/>
/// </summary>
public DateTime UpdateTime { get; private set; } public DateTime UpdateTime { get; private set; }
/// <summary> /// <inheritdoc/>
/// The number of asks in the book
/// </summary>
public int AskCount { get; private set; } public int AskCount { get; private set; }
/// <summary>
/// The number of bids in the book /// <inheritdoc/>
/// </summary>
public int BidCount { get; private set; } public int BidCount { get; private set; }
/// <summary> /// <inheritdoc/>
/// The list of asks
/// </summary>
public IEnumerable<ISymbolOrderBookEntry> Asks public IEnumerable<ISymbolOrderBookEntry> Asks
{ {
get get
@ -139,9 +137,7 @@ namespace CryptoExchange.Net.OrderBook
} }
} }
/// <summary> /// <inheritdoc/>
/// The list of bids
/// </summary>
public IEnumerable<ISymbolOrderBookEntry> Bids public IEnumerable<ISymbolOrderBookEntry> Bids
{ {
get get
@ -151,9 +147,7 @@ namespace CryptoExchange.Net.OrderBook
} }
} }
/// <summary> /// <inheritdoc/>
/// Get a snapshot of the book at this moment
/// </summary>
public (IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks) Book public (IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks) Book
{ {
get get
@ -163,17 +157,7 @@ namespace CryptoExchange.Net.OrderBook
} }
} }
private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry /// <inheritdoc/>
{
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>
public ISymbolOrderBookEntry BestBid public ISymbolOrderBookEntry BestBid
{ {
get get
@ -183,9 +167,7 @@ namespace CryptoExchange.Net.OrderBook
} }
} }
/// <summary> /// <inheritdoc/>
/// The best ask currently in the order book
/// </summary>
public ISymbolOrderBookEntry BestAsk public ISymbolOrderBookEntry BestAsk
{ {
get get
@ -195,9 +177,7 @@ namespace CryptoExchange.Net.OrderBook
} }
} }
/// <summary> /// <inheritdoc/>
/// BestBid/BesAsk returned as a pair
/// </summary>
public (ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers { public (ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers {
get { get {
lock (bookLock) lock (bookLock)
@ -208,9 +188,9 @@ namespace CryptoExchange.Net.OrderBook
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="id"></param> /// <param name="id">The id of the order book. Should be set to {Exchange}[{type}], for example: Kucoin[Spot]</param>
/// <param name="symbol"></param> /// <param name="symbol">The symbol the order book is for</param>
/// <param name="options"></param> /// <param name="options">The options for the order book</param>
protected SymbolOrderBook(string id, string symbol, OrderBookOptions options) protected SymbolOrderBook(string id, string symbol, OrderBookOptions options)
{ {
if (symbol == null) if (symbol == null)
@ -236,10 +216,7 @@ namespace CryptoExchange.Net.OrderBook
log.UpdateWriters(writers.ToList()); log.UpdateWriters(writers.ToList());
} }
/// <summary> /// <inheritdoc/>
/// Start connecting and synchronizing the order book
/// </summary>
/// <returns></returns>
public async Task<CallResult<bool>> StartAsync() public async Task<CallResult<bool>> StartAsync()
{ {
if (Status != OrderBookStatus.Disconnected) if (Status != OrderBookStatus.Disconnected)
@ -275,13 +252,21 @@ namespace CryptoExchange.Net.OrderBook
return new CallResult<bool>(true, null); return new CallResult<bool>(true, null);
} }
/// <summary> /// <inheritdoc/>
/// 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 public async Task StopAsync()
/// at that price since between this calculation and the order placement the book can have changed. {
/// </summary> log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopping");
/// <param name="quantity">The quantity in base asset to fill</param> Status = OrderBookStatus.Disconnected;
/// <param name="type">The type</param> _queueEvent.Set();
/// <returns>Average fill price</returns> 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) public CallResult<decimal> CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type)
{ {
if (Status != OrderBookStatus.Synced) if (Status != OrderBookStatus.Synced)
@ -312,6 +297,219 @@ namespace CryptoExchange.Net.OrderBook
return new CallResult<decimal>(Math.Round(totalCost / totalAmount, 8), null); 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() private void Reset()
{ {
_queueEvent.Set(); _queueEvent.Set();
@ -339,47 +537,6 @@ namespace CryptoExchange.Net.OrderBook
Status = OrderBookStatus.Synced; 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() private void ProcessQueue()
{ {
while (Status != OrderBookStatus.Disconnected) while (Status != OrderBookStatus.Disconnected)
@ -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) private void ProcessRangeUpdates(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{ {
if (lastUpdateId <= LastSequenceNumber) if (lastUpdateId <= LastSequenceNumber)
@ -613,131 +709,6 @@ namespace CryptoExchange.Net.OrderBook
LastSequenceNumber = lastUpdateId; LastSequenceNumber = lastUpdateId;
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{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> internal class DescComparer<T> : IComparer<T>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.Sockets namespace CryptoExchange.Net.Sockets
{ {
/// <summary> /// <summary>
/// Socket connecting /// A single socket connection to the server
/// </summary> /// </summary>
public class SocketConnection public class SocketConnection
{ {
@ -22,26 +22,32 @@ namespace CryptoExchange.Net.Sockets
/// Connection lost event /// Connection lost event
/// </summary> /// </summary>
public event Action? ConnectionLost; public event Action? ConnectionLost;
/// <summary> /// <summary>
/// Connection closed and no reconnect is happening /// Connection closed and no reconnect is happening
/// </summary> /// </summary>
public event Action? ConnectionClosed; public event Action? ConnectionClosed;
/// <summary> /// <summary>
/// Connecting restored event /// Connecting restored event
/// </summary> /// </summary>
public event Action<TimeSpan>? ConnectionRestored; public event Action<TimeSpan>? ConnectionRestored;
/// <summary> /// <summary>
/// The connection is paused event /// The connection is paused event
/// </summary> /// </summary>
public event Action? ActivityPaused; public event Action? ActivityPaused;
/// <summary> /// <summary>
/// The connection is unpaused event /// The connection is unpaused event
/// </summary> /// </summary>
public event Action? ActivityUnpaused; public event Action? ActivityUnpaused;
/// <summary> /// <summary>
/// Connecting closed event /// Connecting closed event
/// </summary> /// </summary>
public event Action? Closed; public event Action? Closed;
/// <summary> /// <summary>
/// Unhandled message event /// Unhandled message event
/// </summary> /// </summary>
@ -57,30 +63,35 @@ namespace CryptoExchange.Net.Sockets
} }
/// <summary> /// <summary>
/// If connection is authenticated /// If the connection has been authenticated
/// </summary> /// </summary>
public bool Authenticated { get; set; } public bool Authenticated { get; set; }
/// <summary> /// <summary>
/// If connection is made /// If connection is made
/// </summary> /// </summary>
public bool Connected { get; private set; } public bool Connected { get; private set; }
/// <summary> /// <summary>
/// The underlying socket /// The underlying websocket
/// </summary> /// </summary>
public IWebsocket Socket { get; set; } public IWebsocket Socket { get; set; }
/// <summary> /// <summary>
/// If the socket should be reconnected upon closing /// If the socket should be reconnected upon closing
/// </summary> /// </summary>
public bool ShouldReconnect { get; set; } public bool ShouldReconnect { get; set; }
/// <summary> /// <summary>
/// Current reconnect try /// Current reconnect try, reset when a successful connection is made
/// </summary> /// </summary>
public int ReconnectTry { get; set; } public int ReconnectTry { get; set; }
/// <summary> /// <summary>
/// Current resubscribe try /// Current resubscribe try, reset when a successful connection is made
/// </summary> /// </summary>
public int ResubscribeTry { get; set; } public int ResubscribeTry { get; set; }
/// <summary> /// <summary>
/// Time of disconnecting /// Time of disconnecting
/// </summary> /// </summary>
@ -138,7 +149,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Process a message received by the socket /// Process a message received by the socket
/// </summary> /// </summary>
/// <param name="data"></param> /// <param name="data">The received data</param>
private void ProcessMessage(string data) private void ProcessMessage(string data)
{ {
var timestamp = DateTime.UtcNow; var timestamp = DateTime.UtcNow;
@ -193,7 +204,7 @@ namespace CryptoExchange.Net.Sockets
} }
/// <summary> /// <summary>
/// Add subscription to this connection /// Add a subscription to this connection
/// </summary> /// </summary>
/// <param name="subscription"></param> /// <param name="subscription"></param>
public void AddSubscription(SocketSubscription subscription) public void AddSubscription(SocketSubscription subscription)
@ -203,15 +214,20 @@ namespace CryptoExchange.Net.Sockets
} }
/// <summary> /// <summary>
/// Get a subscription on this connection /// Get a subscription on this connection by id
/// </summary> /// </summary>
/// <param name="id"></param> /// <param name="id"></param>
public SocketSubscription GetSubscription(int id) public SocketSubscription? GetSubscription(int id)
{ {
lock (subscriptionLock) lock (subscriptionLock)
return subscriptions.SingleOrDefault(s => s.Id == id); 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) private bool HandleData(MessageEvent messageEvent)
{ {
SocketSubscription? currentSubscription = null; SocketSubscription? currentSubscription = null;
@ -249,7 +265,7 @@ namespace CryptoExchange.Net.Sockets
sw.Stop(); sw.Stop();
if (sw.ElapsedMilliseconds > 500) 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."); "Data from this socket may arrive late or not at all if message processing is continuously slow.");
else else
log.Write(LogLevel.Trace, $"Socket {Socket.Id} message processed in {sw.ElapsedMilliseconds}ms"); 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> /// <typeparam name="T">The data type expected in response</typeparam>
/// <param name="obj">The object to send</param> /// <param name="obj">The object to send</param>
/// <param name="timeout">The timeout for response</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> /// <returns></returns>
public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, Func<JToken, bool> handler) public virtual Task SendAndWaitAsync<T>(T obj, TimeSpan timeout, Func<JToken, bool> handler)
{ {
@ -391,6 +407,7 @@ namespace CryptoExchange.Net.Sockets
if (!reconnectResult) if (!reconnectResult)
{ {
ResubscribeTry++; ResubscribeTry++;
DisconnectTime = time;
if (socketClient.ClientOptions.MaxResubscribeTries != null && if (socketClient.ClientOptions.MaxResubscribeTries != null &&
ResubscribeTry >= socketClient.ClientOptions.MaxResubscribeTries) ResubscribeTry >= socketClient.ClientOptions.MaxResubscribeTries)
@ -419,7 +436,7 @@ namespace CryptoExchange.Net.Sockets
if (lostTriggered) if (lostTriggered)
{ {
lostTriggered = false; lostTriggered = false;
InvokeConnectionRestored(time); _ = Task.Run(() => ConnectionRestored?.Invoke(time.HasValue ? DateTime.UtcNow - time.Value : TimeSpan.FromSeconds(0))).ConfigureAwait(false);
} }
break; 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() private async Task<bool> ProcessReconnectAsync()
{ {
if (Authenticated) if (Authenticated)

View File

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

View File

@ -23,7 +23,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <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, /// 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> /// </summary>
public event Action ConnectionClosed public event Action ConnectionClosed
{ {
@ -33,8 +33,8 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting. /// 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 /// 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. This will lead to an incorrect disconnected timespan. /// will only be detected after resuming the code, so the initial disconnect time is lost. Use the timespan only for informational purposes.
/// </summary> /// </summary>
public event Action<TimeSpan> ConnectionRestored public event Action<TimeSpan> ConnectionRestored
{ {