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

Merge pull request #2 from JKorf/master

sync
This commit is contained in:
Artem Kurianov 2020-07-01 18:12:09 +03:00 committed by GitHub
commit 45a15c4d29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 645 additions and 251 deletions

View File

@ -0,0 +1,69 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.OrderBook;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class SymbolOrderBookTests
{
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions("Test", true, false);
private class TestableSymbolOrderBook : SymbolOrderBook
{
public TestableSymbolOrderBook() : base("BTC/USD", defaultOrderBookOptions)
{
}
public override void Dispose() {}
protected override Task<CallResult<bool>> DoResync()
{
throw new NotImplementedException();
}
protected override Task<CallResult<UpdateSubscription>> DoStart()
{
throw new NotImplementedException();
}
}
[TestCase]
public void GivenEmptyBidList_WhenBestBid_ThenEmptySymbolOrderBookEntry()
{
var symbolOrderBook = new TestableSymbolOrderBook();
Assert.IsNotNull(symbolOrderBook.BestBid);
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
}
[TestCase]
public void GivenEmptyAskList_WhenBestAsk_ThenEmptySymbolOrderBookEntry()
{
var symbolOrderBook = new TestableSymbolOrderBook();
Assert.IsNotNull(symbolOrderBook.BestBid);
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
}
[TestCase]
public void GivenEmptyBidAndAskList_WhenBestOffers_ThenEmptySymbolOrderBookEntries()
{
var symbolOrderBook = new TestableSymbolOrderBook();
Assert.IsNotNull(symbolOrderBook.BestOffers);
Assert.IsNotNull(symbolOrderBook.BestOffers.Bid);
Assert.IsNotNull(symbolOrderBook.BestOffers.Ask);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Price);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Quantity);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Price);
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Quantity);
}
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Net.Http;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Logging;
@ -38,14 +39,14 @@ namespace CryptoExchange.Net.UnitTests
{
}
public override Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed)
public override Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, PostParameters postParameters, ArrayParametersSerialization arraySerialization)
{
return base.AddAuthenticationToHeaders(uri, method, parameters, signed);
return base.AddAuthenticationToHeaders(uri, method, parameters, signed, postParameters, arraySerialization);
}
public override Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed)
public override Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, PostParameters postParameters, ArrayParametersSerialization arraySerialization)
{
return base.AddAuthenticationToParameters(uri, method, parameters, signed);
return base.AddAuthenticationToParameters(uri, method, parameters, signed, postParameters, arraySerialization);
}
public override string Sign(string toSign)

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using CryptoExchange.Net.Objects;
using System.Collections.Generic;
using System.Net.Http;
namespace CryptoExchange.Net.Authentication
@ -29,8 +30,11 @@ namespace CryptoExchange.Net.Authentication
/// <param name="method"></param>
/// <param name="parameters"></param>
/// <param name="signed"></param>
/// <param name="postParameterPosition"></param>
/// <param name="arraySerialization"></param>
/// <returns></returns>
public virtual Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed)
public virtual Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed,
PostParameters postParameterPosition, ArrayParametersSerialization arraySerialization)
{
return parameters;
}
@ -42,8 +46,11 @@ namespace CryptoExchange.Net.Authentication
/// <param name="method"></param>
/// <param name="parameters"></param>
/// <param name="signed"></param>
/// <param name="postParameterPosition"></param>
/// <param name="arraySerialization"></param>
/// <returns></returns>
public virtual Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed)
public virtual Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed,
PostParameters postParameterPosition, ArrayParametersSerialization arraySerialization)
{
return new Dictionary<string, string>();
}

View File

@ -6,12 +6,12 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>A base package for implementing cryptocurrency exchange API's</Description>
<PackageVersion>3.0.5</PackageVersion>
<PackageVersion>3.0.11</PackageVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>3.0.5 - Added PausedActivity events on socket subscriptions</PackageReleaseNotes>
<PackageReleaseNotes>3.0.11 - Added support for checksum in SymbolOrderBook</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>8.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>

View File

@ -97,7 +97,7 @@
</summary>
<param name="credentials"></param>
</member>
<member name="M:CryptoExchange.Net.Authentication.AuthenticationProvider.AddAuthenticationToParameters(System.String,System.Net.Http.HttpMethod,System.Collections.Generic.Dictionary{System.String,System.Object},System.Boolean)">
<member name="M:CryptoExchange.Net.Authentication.AuthenticationProvider.AddAuthenticationToParameters(System.String,System.Net.Http.HttpMethod,System.Collections.Generic.Dictionary{System.String,System.Object},System.Boolean,CryptoExchange.Net.Objects.PostParameters,CryptoExchange.Net.Objects.ArrayParametersSerialization)">
<summary>
Add authentication to the parameter list
</summary>
@ -105,9 +105,11 @@
<param name="method"></param>
<param name="parameters"></param>
<param name="signed"></param>
<param name="postParameterPosition"></param>
<param name="arraySerialization"></param>
<returns></returns>
</member>
<member name="M:CryptoExchange.Net.Authentication.AuthenticationProvider.AddAuthenticationToHeaders(System.String,System.Net.Http.HttpMethod,System.Collections.Generic.Dictionary{System.String,System.Object},System.Boolean)">
<member name="M:CryptoExchange.Net.Authentication.AuthenticationProvider.AddAuthenticationToHeaders(System.String,System.Net.Http.HttpMethod,System.Collections.Generic.Dictionary{System.String,System.Object},System.Boolean,CryptoExchange.Net.Objects.PostParameters,CryptoExchange.Net.Objects.ArrayParametersSerialization)">
<summary>
Add authentication to the header dictionary
</summary>
@ -115,6 +117,8 @@
<param name="method"></param>
<param name="parameters"></param>
<param name="signed"></param>
<param name="postParameterPosition"></param>
<param name="arraySerialization"></param>
<returns></returns>
</member>
<member name="M:CryptoExchange.Net.Authentication.AuthenticationProvider.Sign(System.String)">
@ -837,6 +841,11 @@
The best ask currently in the order book
</summary>
</member>
<member name="P:CryptoExchange.Net.Interfaces.ISymbolOrderBook.BestOffers">
<summary>
BestBid/BesAsk returned as a pair
</summary>
</member>
<member name="M:CryptoExchange.Net.Interfaces.ISymbolOrderBook.Start">
<summary>
Start connecting and synchronizing the order book
@ -1144,7 +1153,6 @@
<param name="port">The proxy port</param>
</member>
<member name="M:CryptoExchange.Net.Objects.ApiProxy.#ctor(System.String,System.Int32,System.String,System.String)">
<inheritdoc />
<summary>
Create new settings for a proxy
</summary>
@ -1154,7 +1162,6 @@
<param name="password">The proxy password</param>
</member>
<member name="M:CryptoExchange.Net.Objects.ApiProxy.#ctor(System.String,System.Int32,System.String,System.Security.SecureString)">
<inheritdoc />
<summary>
Create new settings for a proxy
</summary>
@ -1326,6 +1333,11 @@
Connecting
</summary>
</member>
<member name="F:CryptoExchange.Net.Objects.OrderBookStatus.Reconnecting">
<summary>
Reconnecting
</summary>
</member>
<member name="F:CryptoExchange.Net.Objects.OrderBookStatus.Syncing">
<summary>
Syncing data
@ -1538,11 +1550,20 @@
Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped.
</summary>
</member>
<member name="M:CryptoExchange.Net.Objects.OrderBookOptions.#ctor(System.String,System.Boolean)">
<member name="P:CryptoExchange.Net.Objects.OrderBookOptions.StrictLevels">
<summary>
Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10,
when a new bid is added which makes the total amount of bids 11, should the last bid entry be removed
</summary>
</member>
<member name="M:CryptoExchange.Net.Objects.OrderBookOptions.#ctor(System.String,System.Boolean,System.Boolean)">
<summary>
</summary>
<param name="name">The name of the order book implementation</param>
<param name="sequencesAreConsecutive">Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped.</param>
<param name="strictLevels">Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10,
when a new bid is added which makes the total amount of bids 11, should the last bid entry be removed</param>
<param name="levels">Amount of levels for this order book</param>
</member>
<member name="M:CryptoExchange.Net.Objects.OrderBookOptions.ToString">
<inheritdoc />
@ -1659,41 +1680,6 @@
<member name="M:CryptoExchange.Net.Objects.SocketClientOptions.ToString">
<inheritdoc />
</member>
<member name="T:CryptoExchange.Net.OrderBook.ProcessBufferEntry">
<summary>
Buffer entry for order book
</summary>
</member>
<member name="P:CryptoExchange.Net.OrderBook.ProcessBufferEntry.Asks">
<summary>
List of asks
</summary>
</member>
<member name="P:CryptoExchange.Net.OrderBook.ProcessBufferEntry.Bids">
<summary>
List of bids
</summary>
</member>
<member name="T:CryptoExchange.Net.OrderBook.ProcessBufferSingleSequenceEntry">
<summary>
Buffer entry with a single update id per update
</summary>
</member>
<member name="P:CryptoExchange.Net.OrderBook.ProcessBufferSingleSequenceEntry.UpdateId">
<summary>
First update id
</summary>
</member>
<member name="P:CryptoExchange.Net.OrderBook.ProcessBufferSingleSequenceEntry.Asks">
<summary>
List of asks
</summary>
</member>
<member name="P:CryptoExchange.Net.OrderBook.ProcessBufferSingleSequenceEntry.Bids">
<summary>
List of bids
</summary>
</member>
<member name="T:CryptoExchange.Net.OrderBook.ProcessBufferRangeSequenceEntry">
<summary>
Buffer entry with a first and last update id
@ -1754,6 +1740,11 @@
If order book is set
</summary>
</member>
<member name="P:CryptoExchange.Net.OrderBook.SymbolOrderBook.Levels">
<summary>
The amount of levels for this book
</summary>
</member>
<member name="P:CryptoExchange.Net.OrderBook.SymbolOrderBook.Status">
<summary>
The status of the order book. Order book is up to date when the status is `Synced`
@ -1819,6 +1810,11 @@
The best ask currently in the order book
</summary>
</member>
<member name="P:CryptoExchange.Net.OrderBook.SymbolOrderBook.BestOffers">
<summary>
BestBid/BesAsk returned as a pair
</summary>
</member>
<member name="M:CryptoExchange.Net.OrderBook.SymbolOrderBook.#ctor(System.String,CryptoExchange.Net.Objects.OrderBookOptions)">
<summary>
ctor
@ -1867,6 +1863,13 @@
</summary>
<returns></returns>
</member>
<member name="M:CryptoExchange.Net.OrderBook.SymbolOrderBook.DoChecksum(System.Int32)">
<summary>
Validate a checksum with the current order book
</summary>
<param name="checksum"></param>
<returns></returns>
</member>
<member name="M:CryptoExchange.Net.OrderBook.SymbolOrderBook.SetInitialOrderBook(System.Int64,System.Collections.Generic.IEnumerable{CryptoExchange.Net.Interfaces.ISymbolOrderBookEntry},System.Collections.Generic.IEnumerable{CryptoExchange.Net.Interfaces.ISymbolOrderBookEntry})">
<summary>
Set the initial data for the order book
@ -1883,6 +1886,12 @@
<param name="bids"></param>
<param name="asks"></param>
</member>
<member name="M:CryptoExchange.Net.OrderBook.SymbolOrderBook.AddChecksum(System.Int32)">
<summary>
Add a checksum to the process queue
</summary>
<param name="checksum"></param>
</member>
<member name="M:CryptoExchange.Net.OrderBook.SymbolOrderBook.UpdateOrderBook(System.Int64,System.Int64,System.Collections.Generic.IEnumerable{CryptoExchange.Net.Interfaces.ISymbolOrderBookEntry},System.Collections.Generic.IEnumerable{CryptoExchange.Net.Interfaces.ISymbolOrderBookEntry})">
<summary>
Update the order book using a first/last update id
@ -2104,11 +2113,21 @@
Request body content type
</summary>
</member>
<member name="F:CryptoExchange.Net.RestClient.manualParseError">
<summary>
Whether or not we need to manually parse an error instead of relying on the http status code
</summary>
</member>
<member name="F:CryptoExchange.Net.RestClient.arraySerialization">
<summary>
How to serialize array parameters
</summary>
</member>
<member name="F:CryptoExchange.Net.RestClient.requestBodyEmptyContent">
<summary>
What request body should be when no data is send
</summary>
</member>
<member name="P:CryptoExchange.Net.RestClient.RequestTimeout">
<summary>
Timeout for requests
@ -2159,7 +2178,7 @@
</summary>
<returns>The roundtrip time of the ping request</returns>
</member>
<member name="M:CryptoExchange.Net.RestClient.SendRequest``1(System.Uri,System.Net.Http.HttpMethod,System.Threading.CancellationToken,System.Collections.Generic.Dictionary{System.String,System.Object},System.Boolean,System.Boolean)">
<member name="M:CryptoExchange.Net.RestClient.SendRequest``1(System.Uri,System.Net.Http.HttpMethod,System.Threading.CancellationToken,System.Collections.Generic.Dictionary{System.String,System.Object},System.Boolean,System.Boolean,System.Nullable{CryptoExchange.Net.Objects.PostParameters},System.Nullable{CryptoExchange.Net.Objects.ArrayParametersSerialization})">
<summary>
Execute a request
</summary>
@ -2169,7 +2188,9 @@
<param name="cancellationToken">Cancellation token</param>
<param name="parameters">The parameters of the request</param>
<param name="signed">Whether or not the request should be authenticated</param>
<param name="checkResult">Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug)</param>
<param name="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="postPosition">Where the post parameters should be placed</param>
<param name="arraySerialization">How array paramters should be serialized</param>
<returns></returns>
</member>
<member name="M:CryptoExchange.Net.RestClient.GetResponse``1(CryptoExchange.Net.Interfaces.IRequest,System.Threading.CancellationToken)">
@ -2180,7 +2201,15 @@
<param name="cancellationToken">Cancellation token</param>
<returns></returns>
</member>
<member name="M:CryptoExchange.Net.RestClient.ConstructRequest(System.Uri,System.Net.Http.HttpMethod,System.Collections.Generic.Dictionary{System.String,System.Object},System.Boolean)">
<member name="M:CryptoExchange.Net.RestClient.TryParseError(Newtonsoft.Json.Linq.JToken)">
<summary>
Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
This can be used together with ManualParseError to check if it is an error before deserializing to an object
</summary>
<param name="data">Received data</param>
<returns>Null if not an error, Error otherwise</returns>
</member>
<member name="M:CryptoExchange.Net.RestClient.ConstructRequest(System.Uri,System.Net.Http.HttpMethod,System.Collections.Generic.Dictionary{System.String,System.Object},System.Boolean,CryptoExchange.Net.Objects.PostParameters,CryptoExchange.Net.Objects.ArrayParametersSerialization)">
<summary>
Creates a request object
</summary>
@ -2188,6 +2217,8 @@
<param name="method">The method 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="postPosition">Where the post parameters should be placed</param>
<param name="arraySerialization">How array paramters should be serialized</param>
<returns></returns>
</member>
<member name="M:CryptoExchange.Net.RestClient.WriteParamBody(CryptoExchange.Net.Interfaces.IRequest,System.Collections.Generic.Dictionary{System.String,System.Object},System.String)">
@ -2918,5 +2949,148 @@
<member name="M:CryptoExchange.Net.Sockets.WebsocketFactory.CreateWebsocket(CryptoExchange.Net.Logging.Log,System.String,System.Collections.Generic.IDictionary{System.String,System.String},System.Collections.Generic.IDictionary{System.String,System.String})">
<inheritdoc />
</member>
<member name="T:System.Diagnostics.CodeAnalysis.AllowNullAttribute">
<summary>
Specifies that <see langword="null"/> is allowed as an input even if the
corresponding type disallows it.
</summary>
</member>
<member name="M:System.Diagnostics.CodeAnalysis.AllowNullAttribute.#ctor">
<summary>
Initializes a new instance of the <see cref="T:System.Diagnostics.CodeAnalysis.AllowNullAttribute"/> class.
</summary>
</member>
<member name="T:System.Diagnostics.CodeAnalysis.DisallowNullAttribute">
<summary>
Specifies that <see langword="null"/> is disallowed as an input even if the
corresponding type allows it.
</summary>
</member>
<member name="M:System.Diagnostics.CodeAnalysis.DisallowNullAttribute.#ctor">
<summary>
Initializes a new instance of the <see cref="T:System.Diagnostics.CodeAnalysis.DisallowNullAttribute"/> class.
</summary>
</member>
<member name="T:System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute">
<summary>
Specifies that a method that will never return under any circumstance.
</summary>
</member>
<member name="M:System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute.#ctor">
<summary>
Initializes a new instance of the <see cref="T:System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute"/> class.
</summary>
</member>
<member name="T:System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute">
<summary>
Specifies that the method will not return if the associated <see cref="T:System.Boolean"/>
parameter is passed the specified value.
</summary>
</member>
<member name="P:System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute.ParameterValue">
<summary>
Gets the condition parameter value.
Code after the method is considered unreachable by diagnostics if the argument
to the associated parameter matches this value.
</summary>
</member>
<member name="M:System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute.#ctor(System.Boolean)">
<summary>
Initializes a new instance of the <see cref="T:System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute"/>
class with the specified parameter value.
</summary>
<param name="parameterValue">
The condition parameter value.
Code after the method is considered unreachable by diagnostics if the argument
to the associated parameter matches this value.
</param>
</member>
<member name="T:System.Diagnostics.CodeAnalysis.MaybeNullAttribute">
<summary>
Specifies that an output may be <see langword="null"/> even if the
corresponding type disallows it.
</summary>
</member>
<member name="M:System.Diagnostics.CodeAnalysis.MaybeNullAttribute.#ctor">
<summary>
Initializes a new instance of the <see cref="T:System.Diagnostics.CodeAnalysis.MaybeNullAttribute"/> class.
</summary>
</member>
<member name="T:System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute">
<summary>
Specifies that when a method returns <see cref="P:System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute.ReturnValue"/>,
the parameter may be <see langword="null"/> even if the corresponding type disallows it.
</summary>
</member>
<member name="P:System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute.ReturnValue">
<summary>
Gets the return value condition.
If the method returns this value, the associated parameter may be <see langword="null"/>.
</summary>
</member>
<member name="M:System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute.#ctor(System.Boolean)">
<summary>
Initializes the attribute with the specified return value condition.
</summary>
<param name="returnValue">
The return value condition.
If the method returns this value, the associated parameter may be <see langword="null"/>.
</param>
</member>
<member name="T:System.Diagnostics.CodeAnalysis.NotNullAttribute">
<summary>
Specifies that an output is not <see langword="null"/> even if the
corresponding type allows it.
</summary>
</member>
<member name="M:System.Diagnostics.CodeAnalysis.NotNullAttribute.#ctor">
<summary>
Initializes a new instance of the <see cref="T:System.Diagnostics.CodeAnalysis.NotNullAttribute"/> class.
</summary>
</member>
<member name="T:System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute">
<summary>
Specifies that the output will be non-<see langword="null"/> if the
named parameter is non-<see langword="null"/>.
</summary>
</member>
<member name="P:System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute.ParameterName">
<summary>
Gets the associated parameter name.
The output will be non-<see langword="null"/> if the argument to the
parameter specified is non-<see langword="null"/>.
</summary>
</member>
<member name="M:System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute.#ctor(System.String)">
<summary>
Initializes the attribute with the associated parameter name.
</summary>
<param name="parameterName">
The associated parameter name.
The output will be non-<see langword="null"/> if the argument to the
parameter specified is non-<see langword="null"/>.
</param>
</member>
<member name="T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute">
<summary>
Specifies that when a method returns <see cref="P:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute.ReturnValue"/>,
the parameter will not be <see langword="null"/> even if the corresponding type allows it.
</summary>
</member>
<member name="P:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute.ReturnValue">
<summary>
Gets the return value condition.
If the method returns this value, the associated parameter will not be <see langword="null"/>.
</summary>
</member>
<member name="M:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute.#ctor(System.Boolean)">
<summary>
Initializes the attribute with the specified return value condition.
</summary>
<param name="returnValue">
The return value condition.
If the method returns this value, the associated parameter will not be <see langword="null"/>.
</param>
</member>
</members>
</doc>

View File

@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Newtonsoft.Json;
@ -79,16 +80,16 @@ namespace CryptoExchange.Net
foreach (var arrayEntry in arraysParameters)
{
if(serializationType == ArrayParametersSerialization.Array)
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? WebUtility.UrlEncode(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
else
{
var array = (Array)arrayEntry.Value;
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={WebUtility.UrlEncode(a.ToString())}"));
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(a.ToString())}"));
uriString += "&";
}
}
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? WebUtility.UrlEncode(s.Value.ToString()) : s.Value)}"))}";
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(s.Value.ToString()) : s.Value)}"))}";
uriString = uriString.TrimEnd('&');
return uriString;
}

View File

@ -70,6 +70,11 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
ISymbolOrderBookEntry BestAsk { get; }
/// <summary>
/// BestBid/BesAsk returned as a pair
/// </summary>
(ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers { get; }
/// <summary>
/// Start connecting and synchronizing the order book
/// </summary>

View File

@ -1,5 +1,4 @@
using System;
using System.Security;
using System.Security;
namespace CryptoExchange.Net.Objects
{
@ -36,7 +35,6 @@ namespace CryptoExchange.Net.Objects
{
}
/// <inheritdoc />
/// <summary>
/// Create new settings for a proxy
/// </summary>
@ -48,7 +46,6 @@ namespace CryptoExchange.Net.Objects
{
}
/// <inheritdoc />
/// <summary>
/// Create new settings for a proxy
/// </summary>
@ -58,9 +55,6 @@ namespace CryptoExchange.Net.Objects
/// <param name="password">The proxy password</param>
public ApiProxy(string host, int port, string? login, SecureString? password)
{
if (!host.StartsWith("http"))
throw new ArgumentException("Proxy host should start with either http:// or https://");
Host = host;
Port = port;
Login = login;

View File

@ -59,6 +59,10 @@
/// </summary>
Connecting,
/// <summary>
/// Reconnecting
/// </summary>
Reconnecting,
/// <summary>
/// Syncing data
/// </summary>
Syncing,

View File

@ -44,20 +44,30 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public bool SequenceNumbersAreConsecutive { get; }
/// <summary>
/// Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10,
/// when a new bid is added which makes the total amount of bids 11, should the last bid entry be removed
/// </summary>
public bool StrictLevels { get; }
/// <summary>
/// </summary>
/// <param name="name">The name of the order book implementation</param>
/// <param name="sequencesAreConsecutive">Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped.</param>
public OrderBookOptions(string name, bool sequencesAreConsecutive)
/// <param name="strictLevels">Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10,
/// when a new bid is added which makes the total amount of bids 11, should the last bid entry be removed</param>
/// <param name="levels">Amount of levels for this order book</param>
public OrderBookOptions(string name, bool sequencesAreConsecutive, bool strictLevels)
{
OrderBookName = name;
SequenceNumbersAreConsecutive = sequencesAreConsecutive;
StrictLevels = strictLevels;
}
/// <inheritdoc />
public override string ToString()
{
return $"{base.ToString()}, OrderBookName: {OrderBookName}, SequenceNumbersAreConsequtive: {SequenceNumbersAreConsecutive}";
return $"{base.ToString()}, OrderBookName: {OrderBookName}, SequenceNumbersAreConsequtive: {SequenceNumbersAreConsecutive}, StrictLevels: {StrictLevels}";
}
}

View File

@ -3,40 +3,6 @@ using System.Collections.Generic;
namespace CryptoExchange.Net.OrderBook
{
/// <summary>
/// Buffer entry for order book
/// </summary>
public class ProcessBufferEntry
{
/// <summary>
/// List of asks
/// </summary>
public IEnumerable<ISymbolOrderSequencedBookEntry> Asks { get; set; } = new List<ISymbolOrderSequencedBookEntry>();
/// <summary>
/// List of bids
/// </summary>
public IEnumerable<ISymbolOrderSequencedBookEntry> Bids { get; set; } = new List<ISymbolOrderSequencedBookEntry>();
}
/// <summary>
/// Buffer entry with a single update id per update
/// </summary>
public class ProcessBufferSingleSequenceEntry
{
/// <summary>
/// First update id
/// </summary>
public long UpdateId { get; set; }
/// <summary>
/// List of asks
/// </summary>
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = new List<ISymbolOrderBookEntry>();
/// <summary>
/// List of bids
/// </summary>
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = new List<ISymbolOrderBookEntry>();
}
/// <summary>
/// Buffer entry with a first and last update id
/// </summary>

View File

@ -0,0 +1,26 @@
using CryptoExchange.Net.Interfaces;
using System.Collections.Generic;
namespace CryptoExchange.Net.OrderBook
{
internal class ProcessQueueItem
{
public long StartUpdateId { get; set; }
public long EndUpdateId { get; set; }
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = new List<ISymbolOrderBookEntry>();
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = new List<ISymbolOrderBookEntry>();
}
internal class InitialOrderBookItem
{
public long StartUpdateId { get; set; }
public long EndUpdateId { get; set; }
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = new List<ISymbolOrderBookEntry>();
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = new List<ISymbolOrderBookEntry>();
}
internal class ChecksumItem
{
public int Checksum { get; set; }
}
}

View File

@ -1,8 +1,10 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
@ -19,8 +21,7 @@ namespace CryptoExchange.Net.OrderBook
/// <summary>
/// The process buffer, used while syncing
/// </summary>
protected readonly List<object> processBuffer;
private readonly object bookLock = new object();
protected readonly List<ProcessBufferRangeSequenceEntry> processBuffer;
/// <summary>
/// The ask list
/// </summary>
@ -28,11 +29,18 @@ namespace CryptoExchange.Net.OrderBook
/// <summary>
/// The bid list
/// </summary>
protected SortedList<decimal, ISymbolOrderBookEntry> bids;
private readonly object bookLock = new object();
private OrderBookStatus status;
private UpdateSubscription? subscription;
private readonly bool sequencesAreConsecutive;
private readonly bool strictLevels;
private Task _processTask;
private AutoResetEvent _queueEvent;
private ConcurrentQueue<object> _processQueue;
/// <summary>
/// Order book implementation id
@ -48,6 +56,11 @@ namespace CryptoExchange.Net.OrderBook
/// </summary>
protected bool bookSet;
/// <summary>
/// The amount of levels for this book
/// </summary>
protected int? Levels { get; set; } = null;
/// <summary>
/// The status of the order book. Order book is up to date when the status is `Synced`
/// </summary>
@ -127,6 +140,14 @@ namespace CryptoExchange.Net.OrderBook
}
}
private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry
{
public decimal Quantity { get { return 0m; } set {; } }
public decimal Price { get { return 0m; } set {; } }
}
private static ISymbolOrderBookEntry emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry();
/// <summary>
/// The best bid currently in the order book
/// </summary>
@ -135,7 +156,7 @@ namespace CryptoExchange.Net.OrderBook
get
{
lock (bookLock)
return bids.FirstOrDefault().Value;
return bids.FirstOrDefault().Value ?? emptySymbolOrderBookEntry;
}
}
@ -147,7 +168,17 @@ namespace CryptoExchange.Net.OrderBook
get
{
lock (bookLock)
return asks.FirstOrDefault().Value;
return asks.FirstOrDefault().Value ?? emptySymbolOrderBookEntry;
}
}
/// <summary>
/// BestBid/BesAsk returned as a pair
/// </summary>
public (ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers {
get {
lock (bookLock)
return (BestBid,BestAsk);
}
}
@ -165,8 +196,12 @@ namespace CryptoExchange.Net.OrderBook
throw new ArgumentNullException(nameof(options));
Id = options.OrderBookName;
processBuffer = new List<object>();
processBuffer = new List<ProcessBufferRangeSequenceEntry>();
_processQueue = new ConcurrentQueue<object>();
_queueEvent = new AutoResetEvent(false);
sequencesAreConsecutive = options.SequenceNumbersAreConsecutive;
strictLevels = options.StrictLevels;
Symbol = symbol;
Status = OrderBookStatus.Disconnected;
@ -190,7 +225,10 @@ namespace CryptoExchange.Net.OrderBook
/// <returns></returns>
public async Task<CallResult<bool>> StartAsync()
{
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} starting");
Status = OrderBookStatus.Connecting;
_processTask = Task.Run(ProcessQueue);
var startResult = await DoStart().ConfigureAwait(false);
if (!startResult)
return new CallResult<bool>(false, startResult.Error);
@ -205,7 +243,10 @@ namespace CryptoExchange.Net.OrderBook
private void Reset()
{
log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} connection lost");
Status = OrderBookStatus.Connecting;
Status = OrderBookStatus.Reconnecting;
_queueEvent.Set();
// Clear queue
while(_processQueue.TryDequeue(out _))
processBuffer.Clear();
bookSet = false;
DoReset();
@ -240,7 +281,10 @@ namespace CryptoExchange.Net.OrderBook
/// <returns></returns>
public async Task StopAsync()
{
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} stopping");
Status = OrderBookStatus.Disconnected;
_queueEvent.Set();
_processTask.Wait();
if(subscription != null)
await subscription.Close().ConfigureAwait(false);
}
@ -263,12 +307,34 @@ namespace CryptoExchange.Net.OrderBook
protected abstract Task<CallResult<bool>> DoResync();
/// <summary>
/// Set the initial data for the order book
/// Validate a checksum with the current 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)
/// <param name="checksum"></param>
/// <returns></returns>
protected virtual bool DoChecksum(int checksum) => true;
private void ProcessQueue()
{
while(Status != OrderBookStatus.Disconnected)
{
_queueEvent.WaitOne();
while (_processQueue.TryDequeue(out var item))
{
if (Status == OrderBookStatus.Disconnected)
break;
if (item is InitialOrderBookItem iobi)
ProcessInitialOrderBookItem(iobi);
if (item is ProcessQueueItem pqi)
ProcessQueueItem(pqi);
else if (item is ChecksumItem ci)
ProcessChecksum(ci);
}
}
}
private void ProcessInitialOrderBookItem(InitialOrderBookItem item)
{
lock (bookLock)
{
@ -276,31 +342,88 @@ namespace CryptoExchange.Net.OrderBook
return;
asks.Clear();
foreach (var ask in askList)
foreach (var ask in item.Asks)
asks.Add(ask.Price, ask);
bids.Clear();
foreach (var bid in bidList)
foreach (var bid in item.Bids)
bids.Add(bid.Price, bid);
LastSequenceNumber = orderBookSequenceNumber;
LastSequenceNumber = item.EndUpdateId;
AskCount = asks.Count;
BidCount = asks.Count;
BidCount = bids.Count;
bookSet = true;
LastOrderBookUpdate = DateTime.UtcNow;
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{orderBookSequenceNumber}");
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}");
CheckProcessBuffer();
OnOrderBookUpdate?.Invoke(bidList, askList);
OnOrderBookUpdate?.Invoke(item.Asks, item.Bids);
OnBestOffersChanged?.Invoke(BestBid, BestAsk);
}
}
private void CheckBestOffersChanged(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk)
private void ProcessQueueItem(ProcessQueueItem item)
{
if (BestBid.Price != prevBestBid.Price || BestBid.Quantity != prevBestBid.Quantity ||
BestAsk.Price != prevBestAsk.Price || BestAsk.Quantity != prevBestAsk.Quantity)
OnBestOffersChanged?.Invoke(BestBid, BestAsk);
lock (bookLock)
{
if (Status == OrderBookStatus.Connecting || Status == OrderBookStatus.Disconnected)
return;
if (!bookSet)
{
processBuffer.Add(new ProcessBufferRangeSequenceEntry()
{
Asks = item.Asks,
Bids = item.Bids,
FirstUpdateId = item.StartUpdateId,
LastUpdateId = item.EndUpdateId,
});
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{Asks.Count()} asks, {Bids.Count()} bids]");
}
else
{
CheckProcessBuffer();
var (prevBestBid, prevBestAsk) = BestOffers;
ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks);
if (asks.First().Key < bids.First().Key)
{
log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} detected out of sync order book. Resyncing");
_ = subscription?.Reconnect();
return;
}
OnOrderBookUpdate?.Invoke(item.Bids, item.Asks);
CheckBestOffersChanged(prevBestBid, prevBestAsk);
}
}
}
private void ProcessChecksum(ChecksumItem ci)
{
lock (bookLock)
{
var checksumResult = DoChecksum(ci.Checksum);
if(!checksumResult)
{
log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync. Resyncing");
_ = subscription?.Reconnect();
return;
}
}
}
/// <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)
{
bookSet = true;
_processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList });
_queueEvent.Set();
}
/// <summary>
@ -311,31 +434,18 @@ namespace CryptoExchange.Net.OrderBook
/// <param name="asks"></param>
protected void UpdateOrderBook(long rangeUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
lock (bookLock)
{
if (Status == OrderBookStatus.Connecting || Status == OrderBookStatus.Disconnected)
return;
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = rangeUpdateId, EndUpdateId = rangeUpdateId, Asks = asks, Bids = bids });
_queueEvent.Set();
}
if (!bookSet)
{
processBuffer.Add(new ProcessBufferSingleSequenceEntry()
{
UpdateId = rangeUpdateId,
Asks = asks,
Bids = bids
});
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update buffered #{rangeUpdateId}");
}
else
{
CheckProcessBuffer();
var prevBestBid = BestBid;
var prevBestAsk = BestAsk;
ProcessSingleSequenceUpdates(rangeUpdateId, bids, asks);
OnOrderBookUpdate?.Invoke(bids, asks);
CheckBestOffersChanged(prevBestBid, prevBestAsk);
}
}
/// <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>
@ -347,32 +457,8 @@ namespace CryptoExchange.Net.OrderBook
/// <param name="asks"></param>
protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
lock (bookLock)
{
if (Status == OrderBookStatus.Connecting || Status == OrderBookStatus.Disconnected)
return;
if (!bookSet)
{
processBuffer.Add(new ProcessBufferRangeSequenceEntry()
{
Asks = asks,
Bids = bids,
FirstUpdateId = firstUpdateId,
LastUpdateId = lastUpdateId
});
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update buffered #{firstUpdateId}-{lastUpdateId}");
}
else
{
CheckProcessBuffer();
var prevBestBid = BestBid;
var prevBestAsk = BestAsk;
ProcessRangeUpdates(firstUpdateId, lastUpdateId, bids, asks);
OnOrderBookUpdate?.Invoke(bids, asks);
CheckBestOffersChanged(prevBestBid, prevBestAsk);
}
}
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids });
_queueEvent.Set();
}
/// <summary>
@ -382,43 +468,11 @@ namespace CryptoExchange.Net.OrderBook
/// <param name="asks">List of asks</param>
protected void UpdateOrderBook(IEnumerable<ISymbolOrderSequencedBookEntry> bids, IEnumerable<ISymbolOrderSequencedBookEntry> asks)
{
lock (bookLock)
{
if (!bookSet)
{
processBuffer.Add(new ProcessBufferEntry
{
Asks = asks,
Bids = bids
});
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update buffered #{Math.Min(bids.Min(b => b.Sequence), asks.Min(a => a.Sequence))}-{Math.Max(bids.Max(b => b.Sequence), asks.Max(a => a.Sequence))}");
}
else
{
CheckProcessBuffer();
var prevBestBid = BestBid;
var prevBestAsk = BestAsk;
ProcessUpdates(bids, asks);
OnOrderBookUpdate?.Invoke(bids, asks);
CheckBestOffersChanged(prevBestBid, prevBestAsk);
}
}
}
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);
private void ProcessUpdates(IEnumerable<ISymbolOrderSequencedBookEntry> bids, IEnumerable<ISymbolOrderSequencedBookEntry> asks)
{
var entries = new Dictionary<ISymbolOrderSequencedBookEntry, OrderBookEntryType>();
foreach (var entry in asks.OrderBy(a => a.Sequence))
entries.Add(entry, OrderBookEntryType.Ask);
foreach (var entry in bids.OrderBy(a => a.Sequence))
entries.Add(entry, OrderBookEntryType.Bid);
foreach (var entry in entries.OrderBy(e => e.Key.Sequence))
{
if(ProcessUpdate(entry.Key.Sequence, entry.Value, entry.Key))
LastSequenceNumber = entry.Key.Sequence;
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update #{LastSequenceNumber}");
}
_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)
@ -435,28 +489,25 @@ namespace CryptoExchange.Net.OrderBook
foreach (var entry in asks)
ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Ask, entry);
if (Levels.HasValue && strictLevels)
{
while (this.bids.Count() > Levels.Value)
{
BidCount--;
this.bids.Remove(this.bids.Last().Key);
}
while (this.asks.Count() > Levels.Value)
{
AskCount--;
this.asks.Remove(this.asks.Last().Key);
}
}
LastSequenceNumber = lastUpdateId;
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
}
private void ProcessSingleSequenceUpdates(long updateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
foreach (var entry in bids)
{
if (!ProcessUpdate(updateId, OrderBookEntryType.Bid, entry))
return;
}
foreach (var entry in asks)
{
if (!ProcessUpdate(updateId, OrderBookEntryType.Ask, entry))
return;
}
LastSequenceNumber = updateId;
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update processed #{LastSequenceNumber}");
}
/// <summary>
/// Check and empty the process buffer; see what entries to update the book with
/// </summary>
@ -468,13 +519,7 @@ namespace CryptoExchange.Net.OrderBook
foreach (var bufferEntry in pbList)
{
if (bufferEntry is ProcessBufferEntry pbe)
ProcessUpdates(pbe.Bids, pbe.Asks);
else if(bufferEntry is ProcessBufferRangeSequenceEntry pbrse)
ProcessRangeUpdates(pbrse.FirstUpdateId, pbrse.LastUpdateId, pbrse.Bids, pbrse.Asks);
else if (bufferEntry is ProcessBufferSingleSequenceEntry pbsse)
ProcessSingleSequenceUpdates(pbsse.UpdateId, pbsse.Bids, pbsse.Asks);
ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks);
processBuffer.Remove(bufferEntry);
}
}
@ -500,7 +545,6 @@ namespace CryptoExchange.Net.OrderBook
{
// Out of sync
log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
Status = OrderBookStatus.Connecting;
subscription?.Reconnect();
return false;
}
@ -526,7 +570,7 @@ namespace CryptoExchange.Net.OrderBook
}
else
{
listToChange[entry.Price].Quantity = entry.Quantity;
listToChange[entry.Price] = entry;
}
}
@ -552,6 +596,14 @@ namespace CryptoExchange.Net.OrderBook
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>

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
@ -39,11 +40,21 @@ namespace CryptoExchange.Net
/// </summary>
protected RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
/// <summary>
/// Whether or not we need to manually parse an error instead of relying on the http status code
/// </summary>
protected bool manualParseError = false;
/// <summary>
/// How to serialize array parameters
/// </summary>
protected ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array;
/// <summary>
/// What request body should be when no data is send
/// </summary>
protected string requestBodyEmptyContent = "{}";
/// <summary>
/// Timeout for requests
/// </summary>
@ -153,11 +164,13 @@ namespace CryptoExchange.Net
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="parameters">The parameters of the request</param>
/// <param name="signed">Whether or not the request should be authenticated</param>
/// <param name="checkResult">Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug)</param>
/// <param name="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="postPosition">Where the post parameters should be placed</param>
/// <param name="arraySerialization">How array paramters should be serialized</param>
/// <returns></returns>
[return: NotNull]
protected virtual async Task<WebCallResult<T>> SendRequest<T>(Uri uri, HttpMethod method, CancellationToken cancellationToken,
Dictionary<string, object>? parameters = null, bool signed = false, bool checkResult = true) where T : class
Dictionary<string, object>? parameters = null, bool signed = false, bool checkResult = true, PostParameters? postPosition = null, ArrayParametersSerialization? arraySerialization = null) where T : class
{
log.Write(LogVerbosity.Debug, "Creating request for " + uri);
if (signed && authProvider == null)
@ -166,7 +179,7 @@ namespace CryptoExchange.Net
return new WebCallResult<T>(null, null, null, new NoApiCredentialsError());
}
var request = ConstructRequest(uri, method, parameters, signed);
var request = ConstructRequest(uri, method, parameters, signed, postPosition ?? postParametersPosition, arraySerialization ?? this.arraySerialization);
foreach (var limiter in RateLimiters)
{
var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, RateLimitBehaviour);
@ -205,16 +218,38 @@ namespace CryptoExchange.Net
var responseStream = await response.GetResponseStream().ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
var desResult = await Deserialize<T>(responseStream).ConfigureAwait(false);
responseStream.Close();
response.Close();
if (manualParseError)
{
using var reader = new StreamReader(responseStream);
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
responseStream.Close();
response.Close();
log.Write(LogVerbosity.Debug, $"Data received: {data}");
return new WebCallResult<T>(statusCode, headers, desResult.Data, desResult.Error);
var parseResult = ValidateJson(data);
if (!parseResult.Success)
return WebCallResult<T>.CreateErrorResult(response.StatusCode, response.ResponseHeaders, new ServerError(data));
var error = await TryParseError(parseResult.Data);
if(error != null)
return WebCallResult<T>.CreateErrorResult(response.StatusCode, response.ResponseHeaders, error);
var deserializeResult = Deserialize<T>(parseResult.Data);
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, deserializeResult.Data, deserializeResult.Error);
}
else
{
var desResult = await Deserialize<T>(responseStream).ConfigureAwait(false);
responseStream.Close();
response.Close();
return new WebCallResult<T>(statusCode, headers, desResult.Data, desResult.Error);
}
}
else
{
using var reader = new StreamReader(responseStream);
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
log.Write(LogVerbosity.Debug, $"Error received: {data}");
responseStream.Close();
response.Close();
var parseResult = ValidateJson(data);
@ -243,6 +278,17 @@ namespace CryptoExchange.Net
}
}
/// <summary>
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
/// This can be used together with ManualParseError to check if it is an error before deserializing to an object
/// </summary>
/// <param name="data">Received data</param>
/// <returns>Null if not an error, Error otherwise</returns>
protected virtual Task<ServerError?> TryParseError(JToken data)
{
return Task.FromResult<ServerError?>(null);
}
/// <summary>
/// Creates a request object
/// </summary>
@ -250,17 +296,19 @@ namespace CryptoExchange.Net
/// <param name="method">The method 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="postPosition">Where the post parameters should be placed</param>
/// <param name="arraySerialization">How array paramters should be serialized</param>
/// <returns></returns>
protected virtual IRequest ConstructRequest(Uri uri, HttpMethod method, Dictionary<string, object>? parameters, bool signed)
protected virtual IRequest ConstructRequest(Uri uri, HttpMethod method, Dictionary<string, object>? parameters, bool signed, PostParameters postPosition, ArrayParametersSerialization arraySerialization)
{
if (parameters == null)
parameters = new Dictionary<string, object>();
var uriString = uri.ToString();
if(authProvider != null)
parameters = authProvider.AddAuthenticationToParameters(uriString, method, parameters, signed);
parameters = authProvider.AddAuthenticationToParameters(uriString, method, parameters, signed, postPosition, arraySerialization);
if((method == HttpMethod.Get || method == HttpMethod.Delete || postParametersPosition == PostParameters.InUri) && parameters?.Any() == true)
if((method == HttpMethod.Get || method == HttpMethod.Delete || postPosition == PostParameters.InUri) && parameters?.Any() == true)
uriString += "?" + parameters.CreateParamString(true, arraySerialization);
var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
@ -269,17 +317,17 @@ namespace CryptoExchange.Net
var headers = new Dictionary<string, string>();
if (authProvider != null)
headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed);
headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed, postPosition, arraySerialization);
foreach (var header in headers)
request.AddHeader(header.Key, header.Value);
if ((method == HttpMethod.Post || method == HttpMethod.Put) && postParametersPosition != PostParameters.InUri)
if ((method == HttpMethod.Post || method == HttpMethod.Put) && postPosition != PostParameters.InUri)
{
if(parameters?.Any() == true)
WriteParamBody(request, parameters, contentType);
else
request.SetContent("{}", contentType);
request.SetContent(requestBodyEmptyContent, contentType);
}
return request;
@ -302,7 +350,16 @@ namespace CryptoExchange.Net
{
var formData = HttpUtility.ParseQueryString(string.Empty);
foreach (var kvp in parameters.OrderBy(p => p.Key))
formData.Add(kvp.Key, kvp.Value.ToString());
{
if (kvp.Value.GetType().IsArray)
{
var array = (Array)kvp.Value;
foreach(var value in array)
formData.Add(kvp.Key, value.ToString());
}
else
formData.Add(kvp.Key, kvp.Value.ToString());
}
var stringData = formData.ToString();
request.SetContent(stringData, contentType);
}

View File

@ -579,7 +579,7 @@ namespace CryptoExchange.Net
periodicEvent?.Set();
periodicEvent?.Dispose();
log.Write(LogVerbosity.Debug, "Disposing socket client, closing all subscriptions");
UnsubscribeAll().Wait();
Task.Run(UnsubscribeAll).Wait();
semaphoreSlim?.Dispose();
base.Dispose();
}

View File

@ -136,9 +136,16 @@ namespace CryptoExchange.Net.Sockets
private void ProcessMessage(string data)
{
log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} received data: " + data);
if (string.IsNullOrEmpty(data)) return;
var tokenData = data.ToJToken(log);
if (tokenData == null)
return;
{
data = $"\"{data}\"";
tokenData = data.ToJToken(log);
if (tokenData == null)
return;
}
var handledResponse = false;
foreach (var pendingRequest in pendingRequests.ToList())

View File

@ -194,6 +194,27 @@ The order book will automatically reconnect when the connection is lost and resy
To stop synchronizing an order book use the `Stop` method.
## Release notes
* Version 3.0.11 - 20 Jun 2020
* Added support for checksum in SymbolOrderBook
* Version 3.0.10 - 16 Jun 2020
* Fix for order book synchronization
* Version 3.0.9 - 07 Jun 2020
* Added arraySerialization and postParameterPosition to AuthenticationProvider interface
* Fixed array serialization in request body
* Version 3.0.8 - 02 Jun 2020
* Added requestBodyEmptyContent setting for rest client
* Added TryParseError for rest implementations to check for error with success status code
* Version 3.0.7 - 20 May 2020
* Added error debug output
* Fix for unsubscribe causing possible deadlock
* Version 3.0.6 - 03 Mar 2020
* Added BestOffer to SymbolOrderBook, removed invalid check on proxy
* Version 3.0.5 - 05 Feb 2020
* Added PausedActivity events on socket subscriptions