From c3bd04cd8587ac097dfb13a93ce3937ed878ea06 Mon Sep 17 00:00:00 2001 From: Jkorf Date: Thu, 16 Dec 2021 16:32:30 +0100 Subject: [PATCH] docs --- Clients.md | 118 ++++++++++++++++++++++++++++++++++++++++++++- Implementation.md | 2 +- Interfaces.md | 2 + Logging.md | 69 ++++++++++++++++++-------- Migration Guide.md | 23 ++++++++- Options.md | 113 ++++++++++++++++++++++++++++++++++++++++++- Orderbooks.md | 52 +++++++++++++++++++- 7 files changed, 353 insertions(+), 26 deletions(-) create mode 100644 Interfaces.md diff --git a/Clients.md b/Clients.md index 6d3f665..8d1fadb 100644 --- a/Clients.md +++ b/Clients.md @@ -1 +1,117 @@ -WIP \ No newline at end of file +Each implementation generally provides two different clients, which will be the access point for the API's. First of the rest client, which is typically available via [ExchangeName]Client, and a socket client, which is generally named [ExchangeName]SocketClient. For example `BinanceClient` and `BinanceSocketClient`. + +## Rest client +The rest client gives access to the Rest endpoint of the API. Rest endpoints are accessed by sending an HTTP request and receiving a response. The client is split in different sub-clients, which are named API Clients. These API clients are then again split in different topics. Typically a Rest client will look like this: + +- KucoinClient + - SpotApi + - Account + - ExchangeData + - Trading + - FuturesApi + - Account + - ExchangeData + - Trading + +This rest client has 2 different API clients, the `SpotApi` and the `FuturesApi`, each offering their own set of endpoints. +*Requesting ticker info on the spot API* +````C# +var tickersResult = kucoinClient.SpotApi.ExchangeData.GetTickersAsync(); +```` + +Structuring the client like this should make it easier to find endpoints and allows for separate options and functionality for different API clients. For example, some API's have totally separate API's for futures, with different base addresses and different API credentials, while other API's have implemented this in the same API. Either way, this structure can facilitate a similar interface. + +### Rest API client +The Api clients are parts of the total API with a common identifier. In the previous Kucoin example, it separates the Spot and the Futures API. This again is then separated into topics. Most Rest clients implement the following structure: + +**Account** +Endpoints related to the user account. This can for example be endpoints for accessing account settings, or getting account balances. The endpoints in this topic will require API credentials to be provided in the client options. + +**ExchangeData** +Endpoints related to exchange data. Exchange data can be tied to the exchange, for example retrieving the symbols supported by the exchange and what the trading rules are, or can be more general market endpoints, such as getting the most recent trades for a symbol. +These endpoints generally don't require API credentials as they are publicly available. + +**Trading** +Endpoints related to trading. These are endpoints for placing and retrieving orders and retrieving trades made by the user. The endpoints in this topic will require API credentials to be provided in the client options. + +### Processing request responses +Each request will return a WebCallResult with the following properties: +`ResponseHeaders`: The headers returned from the server +`ResponseStatusCode`: The status code as returned by the server +`Success`: Whether or not the call was successful. If successful the `Data` property will contain the resulting data, if not successful the `Error` property will contain more details about what the issue was +`Error`: Details on what went wrong with a call. Only filled when `Success` == `false` +`Data`: Data returned by the server + +When processing the result of a call it should always be checked for success. Not doing so will result in `NullReference` exceptions. + +*Check call result* +````C# +var callResult = await kucoinClient.SpotApi.ExchangeData.GetTickersAsync(); +if(!callResult.Success) +{ + Console.WriteLine("Request failed: " + callResult.Error); + return; +} + +Console.WriteLine("Result: " + callResult.Data); +```` + +## Socket client +The socket client gives access to the websocket API of an exchange. Websocket API's offer streams to which updates are pushed to which a client can listen. Some exchanges also offer some degree of functionality by allowing clients to give commands via the websocket, but most exchanges only allow this via the Rest API. +Just like the Rest client is divided in Rest Api clients, the Socket client is divided into Socket Api clients, each with their own range of API functionality. Socket Api clients are generally not divided into topics since the number of methods isn't as big as with the Rest client. To use the Kucoin client as example again, it looks like this: + +````C# + +- KucoinSocketClient + - SpotStreams + - FuturesStreams + +```` +*Subscribing to updates for all tickers on the Spot Api* +````C# +var subscribeResult = kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler); +```` + +Subscribe methods require a data handler parameter, which is the method which will be called when an update is received from the server. This can be the name of a method or a lambda expression. + +*Method reference* +````C# +await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler); + +private static void DataHandler(DataEvent updateData) +{ + // Process updateData +} +```` + +*Lambda* +````C# +await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(updateData => +{ + // Process updateData +}); +```` + +All updates are wrapped in a `DataEvent<>` object, which contain a `Timestamp`, `OriginalData`, `Topic`, and a `Data` property. The `Timestamp` is the timestamp when the data was received (not send!). `OriginalData` will contain the originally received data if this has been enabled in the client options. `Topic` will contain the topic of the update, which is typically the symbol or asset the update is for. The `Data` property contains the received update data. + +### Processing subscribe responses +Subscribing to a stream will return a `CallResult` object. This should be checked for success the same was as the [rest client](#processing-request-responses). The `UpdateSubscription` object can be used to listen for connection events of the socket connection. +````C# + +var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler); +if(!subscriptionResult.Success) +{ + Console.WriteLine("Failed to connect: " + subscriptionResult.Error); + return; +} +subscriptionResult.Data.ConnectionLost += () => +{ + Console.WriteLine("Connection lost"); +}; +subscriptionResult.Data.ConnectionRestored += (time) => +{ + Console.WriteLine("Connection restored"); +}; + +```` + diff --git a/Implementation.md b/Implementation.md index 6d3f665..989d968 100644 --- a/Implementation.md +++ b/Implementation.md @@ -1 +1 @@ -WIP \ No newline at end of file +TODO steps for creating a new implementation \ No newline at end of file diff --git a/Interfaces.md b/Interfaces.md new file mode 100644 index 0000000..5bcfd64 --- /dev/null +++ b/Interfaces.md @@ -0,0 +1,2 @@ +## IExchangeClient +TODO \ No newline at end of file diff --git a/Logging.md b/Logging.md index e5dc13a..e428412 100644 --- a/Logging.md +++ b/Logging.md @@ -1,12 +1,12 @@ -The library offers extensive logging, for which you can supply your own logging implementation. The logging can be configured via the client options (see XXX). +The library offers extensive logging, for which you can supply your own logging implementation. The logging can be configured via the client options (see XXX). The examples here are using the `BinanceClient` but they should be the same for each implementation. -Logging is based on the `Microsoft.Extensions.Logging.ILogger` interface. This should provide ease of use when trying to connect the library logging to your existing log implementation. +Logging is based on the `Microsoft.Extensions.Logging.ILogger` interface. This should provide ease of use when connecting the library logging to your existing logging implementation. ### Serilog -To make the CryptoExchange.Net logging write to the Serilog logger you can use the following ways, depending on the type of project you're using. The following examples assume that the `Serilog.Sinks.Console` package is already installed. +To make the CryptoExchange.Net logging write to the Serilog logger you can use the following methods, depending on the type of project you're using. The following examples assume that the `Serilog.Sinks.Console` package is already installed. -#### ASP.NET Core/Blazor -With an ASP.Net Core or Blazor project, make sure to install the `Serilog.AspNetCore` package (https://github.com/serilog/serilog-aspnetcore). Adding `UseSerilog` will add the serilogger implementation as an ILogger which you can inject into implementations. +#### Dotnet hosting +With for example an ASP.Net Core or Blazor project the logging can be added to the dependency container, which you can then use to inject it into the client. Make sure to install the `Serilog.AspNetCore` package (https://github.com/serilog/serilog-aspnetcore). Adding `UseSerilog()` in the `CreateHostBuilder` will add the Serilog logging implementation as an ILogger which you can inject into implementations. *Configuring Serilog as ILogger:* ````C# @@ -53,7 +53,7 @@ public class BinanceDataProvider ```` #### Console application -If you don't have a dependency injection service available because you are for example working on a simple console application you use a slightly different approach. +If you don't have a dependency injection service available because you are for example working on a simple console application you can use a slightly different approach. *Configuring Serilog as ILogger:* ````C# @@ -70,22 +70,19 @@ loggerFactory.AddSerilog(serilogLogger); *Injecting ILogger:* ````C# -var client = new BinanceClient(new Binance.Net.Objects.BinanceClientOptions +var client = new BinanceClient(new BinanceClientOptions { LogLevel = LogLevel.Trace, - LogWriters = new List { logger } + LogWriters = new List { loggerFactory.CreateLogger("") } }); ```` -The `BinanceClient` will now use the Serilog logger. +The `BinanceClient` will now write the logging it produces to the Serilog logger. ### Log4Net -To make the CryptoExchange.Net logging write to the Serilog logger you can use the following ways, depending on the type of project you're using. -#### ASP.NET Core/Blazor - -With an ASP.Net Core or Blazor project, make sure to install the `Microsoft.Extensions.Logging.Log4Net.AspNetCore` package (https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore). -Adding `AddLog4Net` will add the Log4Net implementation as an ILogger which you can inject into implementations. Make sure you have a log4net.config configuration file in your project. +To make the CryptoExchange.Net logging write to the Log4Net logge with for example an ASP.Net Core or Blazor project the logging can be added to the dependency container, which you can then use to inject it into the client you're using. Make sure to install the `Microsoft.Extensions.Logging.Log4Net.AspNetCore` package (https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore). +Adding `AddLog4Net()` in the `ConfigureLogging` call will add the Log4Net implementation as an ILogger which you can inject into implementations. Make sure you have a log4net.config configuration file in your project. *Configuring Log4Net as ILogger:* ````C# @@ -122,16 +119,15 @@ public class BinanceDataProvider ```` -#### Console application -TODO +If you don't have the Dotnet dependency container available you'll need to provide your own ILogger implementation. See [Custom logger](#custom-logger). ### NLog To make the CryptoExchange.Net logging write to the NLog logger you can use the following ways, depending on the type of project you're using. -#### ASP.NET Core/Blazor +#### Dotnet hosting -With an ASP.Net Core or Blazor project, make sure to install the `NLog.Web.AspNetCore` package (https://github.com/NLog/NLog/wiki/Getting-started-with-ASP.NET-Core-5). -Adding `UseNLog` will add the NLog implementation as an ILogger which you can inject into implementations. Make sure you have a nlog.config configuration file in your project. +With for example an ASP.Net Core or Blazor project the logging can be added to the dependency container, which you can then use to inject it into the client you're using. Make sure to install the `NLog.Web.AspNetCore` package (https://github.com/NLog/NLog/wiki/Getting-started-with-ASP.NET-Core-5). +Adding `UseNLog()` to the `CreateHostBuilder()` method will add the NLog implementation as an ILogger which you can inject into implementations. Make sure you have a nlog.config configuration file in your project. *Configuring NLog as ILogger:* ````C# @@ -169,5 +165,36 @@ public class BinanceDataProvider ```` -#### Console application -TODO \ No newline at end of file +If you don't have the Dotnet dependency container available you'll need to provide your own ILogger implementation. See [Custom logger](#custom-logger). + +### Custom logger +If you're using a different framework or for some other reason these methods don't work for you you can create a custom ILogger implementation to receive the logging. All you need to do is create an implementation of the ILogger interface and provide that to the client. + +*A simple console logging implementation (note that the ConsoleLogger is already available in the CryptoExchange.Net library)*: +````C# + +public class ConsoleLogger : ILogger +{ + public IDisposable BeginScope(TState state) => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}"; + Console.WriteLine(logMessage); + } +} + +```` + +*Injecting the console logging implementation:* +````C# + +var client = new BinanceClient(new BinanceClientOptions +{ + LogLevel = LogLevel.Trace, + LogWriters = new List { new ConsoleLogger() } +}); + +```` \ No newline at end of file diff --git a/Migration Guide.md b/Migration Guide.md index 6d3f665..28ca9fd 100644 --- a/Migration Guide.md +++ b/Migration Guide.md @@ -1 +1,22 @@ -WIP \ No newline at end of file +Changes from 4.x to 5.x: + +Client structure + binanceClient.Spot.Market.GetTickersAsync(), binanceClient.Spot.System.GetExchangeInfoAsync() => binanceClient.SpotApi.ExchangeData.XXX + binanceSocketClient.Spot.Subscribe => binanceSocketClient.SpotStreams.Subscribe +Options + BaseAddress -> ApiOptions.BaseAddress + +IExchangeClient + IKline + CommonHigh => CommonHighPrice + CommonLow => CommonLowPrice + CommonOpen => CommonOpenPrice + CommonClose => CommonClosePrice + ISymbol + CommonMinimumTradeSize => CommonMinimumTradeQuantity + ITicker + CommonHigh => CommonHighPrice + CommonLow => CommonLowPrice + +ISymbolOrderBook.LastOrderBookUpdate => UpdateTime +Rate limiter \ No newline at end of file diff --git a/Options.md b/Options.md index 6d3f665..3e9da24 100644 --- a/Options.md +++ b/Options.md @@ -1 +1,112 @@ -WIP \ No newline at end of file +## Setting options + +Each implementation can be configured using client options. There are 2 ways to provide these, either via `[client].SetDefaultOptions([options]);`, or in the constructor of the client. The examples here use the `BinanceClient`, but usage is the same for each client. + +*Set the default options to use for new clients* +````C# + +BinanceClient.SetDefaultOptions(new BinanceClientOptions +{ + LogLevel = LogLevel.Trace, + ApiCredentials = new ApiCredentials("KEY", "SECRET") +}); + +```` + +*Set the options to use for a single new client* +````C# + +var client = new BinanceClient(new BinanceClientOptions +{ + LogLevel = LogLevel.Trace, + ApiCredentials = new ApiCredentials("KEY", "SECRET") +}); + +```` + +When calling `SetDefaultOptions` each client created after that will use the options that were set, unless the specific option is overriden in the options that were provided to the client. Consider the following example: +````C# + +BinanceClient.SetDefaultOptions(new BinanceClientOptions +{ + LogLevel = LogLevel.Trace, + OutputOriginalData = true +}); + +var client = new BinanceClient(new BinanceClientOptions +{ + LogLevel = LogLevel.Debug, + ApiCredentials = new ApiCredentials("KEY", "SECRET") +}); + +```` + +The client instance will have the following options: +`LogLevel = Debug` +`OutputOriginalData = true` +`ApiCredentials = set` + +## Api options +The options are divided in two categories. The basic options, which will apply to everything the client does, and the Api options, which is limited to the specific API client (see XXXclients). + +````C# + +var client = new BinanceClient(new BinanceClientOptions +{ + LogLevel = LogLevel.Debug, + ApiCredentials = new ApiCredentials("GENERAL-KEY", "GENERAL-SECRET"), + SpotApiOptions = new BinanceApiClientOptions + { + ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET") , + BaseAddress = BinanceApiAddresses.Us.RestClientAddress + } +}); + +```` + +The options provided in the SpotApiOptions are only applied to the SpotApi (`client.SpotApi.XXX` endpoints), while the base options are applied to everything. This means that the spot endpoints will use the "SPOT-KEY" credentials, while all other endpoints (`client.UsdFuturesApi.XXX` / `client.CoinFuturesApi.XXX`) will use the "GENERAL-KEY" credentials. + +## CryptoExchange.Net options definitions +All clients have access to the following options, specific implementations might have additional options. + +**Base client options** +|Option|Description|Default| +|------|-----------|-------| +|`LogWriters`| A list of `ILogger`s to handle log messages. | `new List { new DebugLogger() }` | +|`LogLevel`| The minimum log level before passing messages to the `LogWriters`. Messages with a more verbose level than the one specified here will be ignored. Setting this to `null` will pass all messages to the `LogWriters`.| `LogLevel.Information` +|`OutputOriginalData`|If set to `true` the originally received Json data will be output as well as the deserialized object. For `RestClient` calls the data will be in the `WebCallResult.OriginalData` property, for `SocketClient` subscriptions the data will be available in the `DataEvent.OriginalData` property when receiving an update. | `false` +|`ApiCredentials`| The API credentials to use for accessing protected endpoints. Typically a key/secret combination. Note that this is a `default` value for all API clients, and can be overridden per API client. See the `Base Api client options`| `null` +|`Proxy`|The proxy to use for connecting to the API.| `null` + +**Rest client options (extension of base client options)** +|Option|Description|Default| +|------|-----------|-------| +|`RequestTimeout`|The time out to use for requests.|`TimeSpan.FromSeconds(30)`| +|`HttpClient`|The `HttpClient` instance to use for making requests. When creating multiple `RestClient` instances a single `HttpClient` should be provided to prevent each client instance from creating its own. *[WARNING] When providing the `HttpClient` instance in the options both the `RequestTimeout` and `Proxy` client options will be ignored and should be set on the provided `HttpClient` instance.*| `null` | + +**Socket client options (extension of base client options)** +|Option|Description|Default| +|------|-----------|-------| +|`AutoReconnect`|Whether or not the socket should automatically reconnect when disconnected.|`true` +|`ReconnectInterval`|The time to wait between connection tries when reconnecting.|`TimeSpan.FromSeconds(5)` +|`SocketResponseTimeout`|The time in which a response is expected on a request before giving a timeout.|`TimeSpan.FromSeconds(10)` +|`SocketNoDataTimeout`|If no data is received after this timespan then assume the connection is dropped. This is mainly used for API's which have some sort of ping/keepalive system. For example; the Bitfinex API will sent a heartbeat message every 15 seconds, so the `SocketNoDataTimeout` could be set to 20 seconds. On API's without such a mechanism this might not work because there just might not be any update while still being fully connected. | `default(TimeSpan)` (no timeout) +|`SocketSubscriptionsCombineTarget`|The amount of subscriptions that should be made on a single socket connection. Not all exchanges support multiple subscriptions on a single socket. Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a single connection will also increase the amount of traffic on that single connection, potentially leading to issues.| Depends on implementation +|`MaxReconnectTries`|The maximum amount of tries for reconnecting|`null` (infinite) +|`MaxResubscribeTries`|The maximum amount of tries for resubscribing after successfully reconnecting the socket|5 +|`MaxConcurrentResubscriptionsPerSocket`|The maximum number of concurrent resubscriptions per socket when resubscribing after reconnecting|5 + +**Base Api client options** +|Option|Description|Default| +|------|-----------|-------| +|`ApiCredentials`|The API credentials to use for this specific API client. Will override any credentials provided in the base client options| +|`BaseAddress`|The base address to the API. All calls to the API will use this base address as basis for the endpoints. This allows for swapping to test API's or swapping to a different cluster for example.|Depends on implementation + +**Options for Rest Api Client (extension of base api client options)** +|Option|Description|Default| +|------|-----------|-------| +|`RateLimiters`|A list of `IRateLimiter`s to use.|`new List()`| +|`RateLimitingBehaviour`|What should happen when a rate limit is reached.|`RateLimitingBehaviour.Wait`| + +**Options for Socket Api Client (extension of base api client options)** +There are currently no specific options for socket API clients, the base API options are still available. diff --git a/Orderbooks.md b/Orderbooks.md index 6d3f665..83d6274 100644 --- a/Orderbooks.md +++ b/Orderbooks.md @@ -1 +1,51 @@ -WIP \ No newline at end of file +Each implementation provides an order book implementation. These implementations will provide a client side order book and will take care of synchronization with the server, and will handle reconnecting and resynchronizing in case of a dropped connection. +Order book implementations are named as `[ExchangeName][Type]SymbolOrderBook`, for example `BinanceSpotSymbolOrderBook`. + +## Usage +Start the book synchronization by calling the `StartAsync` method. This returns a success state whether the book is successfully synchronized and started. You can listen to the `OnStatusChange` event to be notified of when the status of a book changes. Note that the order book is only synchronized with the server when the state is `Synced`. + +*Start an order book and print the top 3 rows* +````C# + +var book = new BinanceSpotSymbolOrderBook("BTCUSDT"); +book.OnStatusChange += (oldState, newState) => Console.WriteLine($"State changed from {oldState} to {newState}"); +var startResult = await book.StartAsync(); +if (!startResult.Success) +{ + Console.WriteLine("Failed to start order book: " + startResult.Error); + return; +} + +while(true) +{ + Console.WriteLine(book.ToString(3); + await Task.Delay(500); +} + +```` + +### Accessing bids/asks +You can access the current Bid/Ask lists using the responding properties: +`var currentBidList = book.Bids;` +`var currentAskList = book.Asks;` + +Note that these will return copies of the internally synced lists when accessing the properties, and when accessing them in sequence like above does mean that the lists may not be in sync with eachother since they're accessed at different points in time. +When you need both lists in sync you should access the `Book` property. +`var (currentBidList, currentAskList) = book.Book;` + +Because copies of the lists are made when accessing the bids/asks properties the performance impact should be considered. When only the current best ask/bid info is needed you can access the `BestOffers` property. +`var (bestBid, bestAsk) = book.BestOffers;` + +### Events +The following events are available on the symbol order book: +`book.OnStatusChange`: The book has changed state. This happens during connecting, the connection was lost or the order book was detected to be out of sync. The asks/bids are only the actual with the server when state is `Synced`. +`book.OnOrderBookUpdate`: The book has changed, the arguments contain the changed entries. +`book.OnBestOffersChanged`: The best offer (best bid, best ask) has changed. + +````C# + +book.OnStatusChange += (oldStatus, newStatus) => { Console.WriteLine($"State changed from {oldStatus} to {newStatus}"); }; +book.OnOrderBookUpdate += (bidsAsks) => { Console.WriteLine($"Order book changed: {bidsAsks.Asks.Count()} asks, {bidsAsks.Bids.Count()} bids"); }; +book.OnBestOffersChanged += (bestOffer) => { Console.WriteLine($"Best offer changed, best bid: {bestOffer.BestBid.Price}, best ask: {bestOffer.BestAsk.Price}"); }; + +```` \ No newline at end of file