1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-06-11 01:46:12 +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 System.Net.Http;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Logging; 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) 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; using System.Net.Http;
namespace CryptoExchange.Net.Authentication namespace CryptoExchange.Net.Authentication
@ -29,8 +30,11 @@ namespace CryptoExchange.Net.Authentication
/// <param name="method"></param> /// <param name="method"></param>
/// <param name="parameters"></param> /// <param name="parameters"></param>
/// <param name="signed"></param> /// <param name="signed"></param>
/// <param name="postParameterPosition"></param>
/// <param name="arraySerialization"></param>
/// <returns></returns> /// <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; return parameters;
} }
@ -42,8 +46,11 @@ namespace CryptoExchange.Net.Authentication
/// <param name="method"></param> /// <param name="method"></param>
/// <param name="parameters"></param> /// <param name="parameters"></param>
/// <param name="signed"></param> /// <param name="signed"></param>
/// <param name="postParameterPosition"></param>
/// <param name="arraySerialization"></param>
/// <returns></returns> /// <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>(); return new Dictionary<string, string>();
} }

View File

@ -6,12 +6,12 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>A base package for implementing cryptocurrency exchange API's</Description> <Description>A base package for implementing cryptocurrency exchange API's</Description>
<PackageVersion>3.0.5</PackageVersion> <PackageVersion>3.0.11</PackageVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl> <PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage> <NeutralLanguage>en</NeutralLanguage>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <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> <Nullable>enable</Nullable>
<LangVersion>8.0</LangVersion> <LangVersion>8.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>

View File

@ -97,7 +97,7 @@
</summary> </summary>
<param name="credentials"></param> <param name="credentials"></param>
</member> </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> <summary>
Add authentication to the parameter list Add authentication to the parameter list
</summary> </summary>
@ -105,9 +105,11 @@
<param name="method"></param> <param name="method"></param>
<param name="parameters"></param> <param name="parameters"></param>
<param name="signed"></param> <param name="signed"></param>
<param name="postParameterPosition"></param>
<param name="arraySerialization"></param>
<returns></returns> <returns></returns>
</member> </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> <summary>
Add authentication to the header dictionary Add authentication to the header dictionary
</summary> </summary>
@ -115,6 +117,8 @@
<param name="method"></param> <param name="method"></param>
<param name="parameters"></param> <param name="parameters"></param>
<param name="signed"></param> <param name="signed"></param>
<param name="postParameterPosition"></param>
<param name="arraySerialization"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:CryptoExchange.Net.Authentication.AuthenticationProvider.Sign(System.String)"> <member name="M:CryptoExchange.Net.Authentication.AuthenticationProvider.Sign(System.String)">
@ -837,6 +841,11 @@
The best ask currently in the order book The best ask currently in the order book
</summary> </summary>
</member> </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"> <member name="M:CryptoExchange.Net.Interfaces.ISymbolOrderBook.Start">
<summary> <summary>
Start connecting and synchronizing the order book Start connecting and synchronizing the order book
@ -1144,7 +1153,6 @@
<param name="port">The proxy port</param> <param name="port">The proxy port</param>
</member> </member>
<member name="M:CryptoExchange.Net.Objects.ApiProxy.#ctor(System.String,System.Int32,System.String,System.String)"> <member name="M:CryptoExchange.Net.Objects.ApiProxy.#ctor(System.String,System.Int32,System.String,System.String)">
<inheritdoc />
<summary> <summary>
Create new settings for a proxy Create new settings for a proxy
</summary> </summary>
@ -1154,7 +1162,6 @@
<param name="password">The proxy password</param> <param name="password">The proxy password</param>
</member> </member>
<member name="M:CryptoExchange.Net.Objects.ApiProxy.#ctor(System.String,System.Int32,System.String,System.Security.SecureString)"> <member name="M:CryptoExchange.Net.Objects.ApiProxy.#ctor(System.String,System.Int32,System.String,System.Security.SecureString)">
<inheritdoc />
<summary> <summary>
Create new settings for a proxy Create new settings for a proxy
</summary> </summary>
@ -1326,6 +1333,11 @@
Connecting Connecting
</summary> </summary>
</member> </member>
<member name="F:CryptoExchange.Net.Objects.OrderBookStatus.Reconnecting">
<summary>
Reconnecting
</summary>
</member>
<member name="F:CryptoExchange.Net.Objects.OrderBookStatus.Syncing"> <member name="F:CryptoExchange.Net.Objects.OrderBookStatus.Syncing">
<summary> <summary>
Syncing data Syncing data
@ -1538,11 +1550,20 @@
Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped. Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped.
</summary> </summary>
</member> </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>
</summary> </summary>
<param name="name">The name of the order book implementation</param> <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="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>
<member name="M:CryptoExchange.Net.Objects.OrderBookOptions.ToString"> <member name="M:CryptoExchange.Net.Objects.OrderBookOptions.ToString">
<inheritdoc /> <inheritdoc />
@ -1659,41 +1680,6 @@
<member name="M:CryptoExchange.Net.Objects.SocketClientOptions.ToString"> <member name="M:CryptoExchange.Net.Objects.SocketClientOptions.ToString">
<inheritdoc /> <inheritdoc />
</member> </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"> <member name="T:CryptoExchange.Net.OrderBook.ProcessBufferRangeSequenceEntry">
<summary> <summary>
Buffer entry with a first and last update id Buffer entry with a first and last update id
@ -1754,6 +1740,11 @@
If order book is set If order book is set
</summary> </summary>
</member> </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"> <member name="P:CryptoExchange.Net.OrderBook.SymbolOrderBook.Status">
<summary> <summary>
The status of the order book. Order book is up to date when the status is `Synced` 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 The best ask currently in the order book
</summary> </summary>
</member> </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)"> <member name="M:CryptoExchange.Net.OrderBook.SymbolOrderBook.#ctor(System.String,CryptoExchange.Net.Objects.OrderBookOptions)">
<summary> <summary>
ctor ctor
@ -1867,6 +1863,13 @@
</summary> </summary>
<returns></returns> <returns></returns>
</member> </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})"> <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> <summary>
Set the initial data for the order book Set the initial data for the order book
@ -1883,6 +1886,12 @@
<param name="bids"></param> <param name="bids"></param>
<param name="asks"></param> <param name="asks"></param>
</member> </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})"> <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> <summary>
Update the order book using a first/last update id Update the order book using a first/last update id
@ -2104,11 +2113,21 @@
Request body content type Request body content type
</summary> </summary>
</member> </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"> <member name="F:CryptoExchange.Net.RestClient.arraySerialization">
<summary> <summary>
How to serialize array parameters How to serialize array parameters
</summary> </summary>
</member> </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"> <member name="P:CryptoExchange.Net.RestClient.RequestTimeout">
<summary> <summary>
Timeout for requests Timeout for requests
@ -2159,7 +2178,7 @@
</summary> </summary>
<returns>The roundtrip time of the ping request</returns> <returns>The roundtrip time of the ping request</returns>
</member> </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> <summary>
Execute a request Execute a request
</summary> </summary>
@ -2169,7 +2188,9 @@
<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="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> <returns></returns>
</member> </member>
<member name="M:CryptoExchange.Net.RestClient.GetResponse``1(CryptoExchange.Net.Interfaces.IRequest,System.Threading.CancellationToken)"> <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> <param name="cancellationToken">Cancellation token</param>
<returns></returns> <returns></returns>
</member> </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> <summary>
Creates a request object Creates a request object
</summary> </summary>
@ -2188,6 +2217,8 @@
<param name="method">The method of the request</param> <param name="method">The method of the request</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="postPosition">Where the post parameters should be placed</param>
<param name="arraySerialization">How array paramters should be serialized</param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:CryptoExchange.Net.RestClient.WriteParamBody(CryptoExchange.Net.Interfaces.IRequest,System.Collections.Generic.Dictionary{System.String,System.Object},System.String)"> <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})"> <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 /> <inheritdoc />
</member> </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> </members>
</doc> </doc>

View File

@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
using System.Security; using System.Security;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web;
using CryptoExchange.Net.Logging; using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -79,16 +80,16 @@ namespace CryptoExchange.Net
foreach (var arrayEntry in arraysParameters) foreach (var arrayEntry in arraysParameters)
{ {
if(serializationType == ArrayParametersSerialization.Array) 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 else
{ {
var array = (Array)arrayEntry.Value; 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 += "&";
} }
} }
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('&'); uriString = uriString.TrimEnd('&');
return uriString; return uriString;
} }

View File

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

View File

@ -1,5 +1,4 @@
using System; using System.Security;
using System.Security;
namespace CryptoExchange.Net.Objects namespace CryptoExchange.Net.Objects
{ {
@ -36,7 +35,6 @@ namespace CryptoExchange.Net.Objects
{ {
} }
/// <inheritdoc />
/// <summary> /// <summary>
/// Create new settings for a proxy /// Create new settings for a proxy
/// </summary> /// </summary>
@ -48,7 +46,6 @@ namespace CryptoExchange.Net.Objects
{ {
} }
/// <inheritdoc />
/// <summary> /// <summary>
/// Create new settings for a proxy /// Create new settings for a proxy
/// </summary> /// </summary>
@ -58,9 +55,6 @@ namespace CryptoExchange.Net.Objects
/// <param name="password">The proxy password</param> /// <param name="password">The proxy password</param>
public ApiProxy(string host, int port, string? login, SecureString? password) 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; Host = host;
Port = port; Port = port;
Login = login; Login = login;

View File

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

View File

@ -44,20 +44,30 @@ namespace CryptoExchange.Net.Objects
/// </summary> /// </summary>
public bool SequenceNumbersAreConsecutive { get; } 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>
/// </summary> /// </summary>
/// <param name="name">The name of the order book implementation</param> /// <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="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; OrderBookName = name;
SequenceNumbersAreConsecutive = sequencesAreConsecutive; SequenceNumbersAreConsecutive = sequencesAreConsecutive;
StrictLevels = strictLevels;
} }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() 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 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> /// <summary>
/// Buffer entry with a first and last update id /// Buffer entry with a first and last update id
/// </summary> /// </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;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging; using CryptoExchange.Net.Logging;
@ -19,8 +21,7 @@ namespace CryptoExchange.Net.OrderBook
/// <summary> /// <summary>
/// The process buffer, used while syncing /// The process buffer, used while syncing
/// </summary> /// </summary>
protected readonly List<object> processBuffer; protected readonly List<ProcessBufferRangeSequenceEntry> processBuffer;
private readonly object bookLock = new object();
/// <summary> /// <summary>
/// The ask list /// The ask list
/// </summary> /// </summary>
@ -28,11 +29,18 @@ namespace CryptoExchange.Net.OrderBook
/// <summary> /// <summary>
/// The bid list /// The bid list
/// </summary> /// </summary>
protected SortedList<decimal, ISymbolOrderBookEntry> bids; protected SortedList<decimal, ISymbolOrderBookEntry> bids;
private readonly object bookLock = new object();
private OrderBookStatus status; private OrderBookStatus status;
private UpdateSubscription? subscription; private UpdateSubscription? subscription;
private readonly bool sequencesAreConsecutive; private readonly bool sequencesAreConsecutive;
private readonly bool strictLevels;
private Task _processTask;
private AutoResetEvent _queueEvent;
private ConcurrentQueue<object> _processQueue;
/// <summary> /// <summary>
/// Order book implementation id /// Order book implementation id
@ -48,6 +56,11 @@ namespace CryptoExchange.Net.OrderBook
/// </summary> /// </summary>
protected bool bookSet; protected bool bookSet;
/// <summary>
/// The amount of levels for this book
/// </summary>
protected int? Levels { get; set; } = null;
/// <summary> /// <summary>
/// The status of the order book. Order book is up to date when the status is `Synced` /// The status of the order book. Order book is up to date when the status is `Synced`
/// </summary> /// </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> /// <summary>
/// The best bid currently in the order book /// The best bid currently in the order book
/// </summary> /// </summary>
@ -135,7 +156,7 @@ namespace CryptoExchange.Net.OrderBook
get get
{ {
lock (bookLock) lock (bookLock)
return bids.FirstOrDefault().Value; return bids.FirstOrDefault().Value ?? emptySymbolOrderBookEntry;
} }
} }
@ -147,7 +168,17 @@ namespace CryptoExchange.Net.OrderBook
get get
{ {
lock (bookLock) 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)); throw new ArgumentNullException(nameof(options));
Id = options.OrderBookName; Id = options.OrderBookName;
processBuffer = new List<object>(); processBuffer = new List<ProcessBufferRangeSequenceEntry>();
_processQueue = new ConcurrentQueue<object>();
_queueEvent = new AutoResetEvent(false);
sequencesAreConsecutive = options.SequenceNumbersAreConsecutive; sequencesAreConsecutive = options.SequenceNumbersAreConsecutive;
strictLevels = options.StrictLevels;
Symbol = symbol; Symbol = symbol;
Status = OrderBookStatus.Disconnected; Status = OrderBookStatus.Disconnected;
@ -190,7 +225,10 @@ namespace CryptoExchange.Net.OrderBook
/// <returns></returns> /// <returns></returns>
public async Task<CallResult<bool>> StartAsync() public async Task<CallResult<bool>> StartAsync()
{ {
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} starting");
Status = OrderBookStatus.Connecting; Status = OrderBookStatus.Connecting;
_processTask = Task.Run(ProcessQueue);
var startResult = await DoStart().ConfigureAwait(false); var startResult = await DoStart().ConfigureAwait(false);
if (!startResult) if (!startResult)
return new CallResult<bool>(false, startResult.Error); return new CallResult<bool>(false, startResult.Error);
@ -205,7 +243,10 @@ namespace CryptoExchange.Net.OrderBook
private void Reset() private void Reset()
{ {
log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} connection lost"); 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(); processBuffer.Clear();
bookSet = false; bookSet = false;
DoReset(); DoReset();
@ -240,7 +281,10 @@ namespace CryptoExchange.Net.OrderBook
/// <returns></returns> /// <returns></returns>
public async Task StopAsync() public async Task StopAsync()
{ {
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} stopping");
Status = OrderBookStatus.Disconnected; Status = OrderBookStatus.Disconnected;
_queueEvent.Set();
_processTask.Wait();
if(subscription != null) if(subscription != null)
await subscription.Close().ConfigureAwait(false); await subscription.Close().ConfigureAwait(false);
} }
@ -263,12 +307,34 @@ namespace CryptoExchange.Net.OrderBook
protected abstract Task<CallResult<bool>> DoResync(); protected abstract Task<CallResult<bool>> DoResync();
/// <summary> /// <summary>
/// Set the initial data for the order book /// Validate a checksum with the current order book
/// </summary> /// </summary>
/// <param name="orderBookSequenceNumber">The last update sequence number</param> /// <param name="checksum"></param>
/// <param name="askList">List of asks</param> /// <returns></returns>
/// <param name="bidList">List of bids</param> protected virtual bool DoChecksum(int checksum) => true;
protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable<ISymbolOrderBookEntry> bidList, IEnumerable<ISymbolOrderBookEntry> askList)
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) lock (bookLock)
{ {
@ -276,31 +342,88 @@ namespace CryptoExchange.Net.OrderBook
return; return;
asks.Clear(); asks.Clear();
foreach (var ask in askList) foreach (var ask in item.Asks)
asks.Add(ask.Price, ask); asks.Add(ask.Price, ask);
bids.Clear(); bids.Clear();
foreach (var bid in bidList) foreach (var bid in item.Bids)
bids.Add(bid.Price, bid); bids.Add(bid.Price, bid);
LastSequenceNumber = orderBookSequenceNumber; LastSequenceNumber = item.EndUpdateId;
AskCount = asks.Count; AskCount = asks.Count;
BidCount = asks.Count; BidCount = bids.Count;
bookSet = true;
LastOrderBookUpdate = DateTime.UtcNow; 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(); CheckProcessBuffer();
OnOrderBookUpdate?.Invoke(bidList, askList); OnOrderBookUpdate?.Invoke(item.Asks, item.Bids);
OnBestOffersChanged?.Invoke(BestBid, BestAsk); 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 || lock (bookLock)
BestAsk.Price != prevBestAsk.Price || BestAsk.Quantity != prevBestAsk.Quantity) {
OnBestOffersChanged?.Invoke(BestBid, BestAsk); 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> /// <summary>
@ -311,31 +434,18 @@ namespace CryptoExchange.Net.OrderBook
/// <param name="asks"></param> /// <param name="asks"></param>
protected void UpdateOrderBook(long rangeUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks) protected void UpdateOrderBook(long rangeUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{ {
lock (bookLock) _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = rangeUpdateId, EndUpdateId = rangeUpdateId, Asks = asks, Bids = bids });
{ _queueEvent.Set();
if (Status == OrderBookStatus.Connecting || Status == OrderBookStatus.Disconnected) }
return;
if (!bookSet) /// <summary>
{ /// Add a checksum to the process queue
processBuffer.Add(new ProcessBufferSingleSequenceEntry() /// </summary>
{ /// <param name="checksum"></param>
UpdateId = rangeUpdateId, protected void AddChecksum(int checksum)
Asks = asks, {
Bids = bids _processQueue.Enqueue(new ChecksumItem() { Checksum = checksum });
}); _queueEvent.Set();
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> /// <summary>
@ -347,32 +457,8 @@ namespace CryptoExchange.Net.OrderBook
/// <param name="asks"></param> /// <param name="asks"></param>
protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks) protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{ {
lock (bookLock) _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids });
{ _queueEvent.Set();
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);
}
}
} }
/// <summary> /// <summary>
@ -382,43 +468,11 @@ namespace CryptoExchange.Net.OrderBook
/// <param name="asks">List of asks</param> /// <param name="asks">List of asks</param>
protected void UpdateOrderBook(IEnumerable<ISymbolOrderSequencedBookEntry> bids, IEnumerable<ISymbolOrderSequencedBookEntry> asks) protected void UpdateOrderBook(IEnumerable<ISymbolOrderSequencedBookEntry> bids, IEnumerable<ISymbolOrderSequencedBookEntry> asks)
{ {
lock (bookLock) 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);
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);
}
}
}
private void ProcessUpdates(IEnumerable<ISymbolOrderSequencedBookEntry> bids, IEnumerable<ISymbolOrderSequencedBookEntry> asks) _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = lowest, EndUpdateId = highest , Asks = asks, Bids = bids });
{ _queueEvent.Set();
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}");
}
} }
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)
@ -435,28 +489,25 @@ namespace CryptoExchange.Net.OrderBook
foreach (var entry in asks) foreach (var entry in asks)
ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Ask, entry); 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; LastSequenceNumber = lastUpdateId;
log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{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> /// <summary>
/// Check and empty the process buffer; see what entries to update the book with /// Check and empty the process buffer; see what entries to update the book with
/// </summary> /// </summary>
@ -468,13 +519,7 @@ namespace CryptoExchange.Net.OrderBook
foreach (var bufferEntry in pbList) foreach (var bufferEntry in pbList)
{ {
if (bufferEntry is ProcessBufferEntry pbe) ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks);
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);
processBuffer.Remove(bufferEntry); processBuffer.Remove(bufferEntry);
} }
} }
@ -500,7 +545,6 @@ namespace CryptoExchange.Net.OrderBook
{ {
// Out of sync // Out of sync
log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting"); log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
Status = OrderBookStatus.Connecting;
subscription?.Reconnect(); subscription?.Reconnect();
return false; return false;
} }
@ -526,7 +570,7 @@ namespace CryptoExchange.Net.OrderBook
} }
else 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); 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> /// <summary>
/// Dispose the order book /// Dispose the order book
/// </summary> /// </summary>

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Net.Sockets; using System.Net.Sockets;
using System.Security.Cryptography;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
@ -39,11 +40,21 @@ namespace CryptoExchange.Net
/// </summary> /// </summary>
protected RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json; 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> /// <summary>
/// How to serialize array parameters /// How to serialize array parameters
/// </summary> /// </summary>
protected ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array; protected ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array;
/// <summary>
/// What request body should be when no data is send
/// </summary>
protected string requestBodyEmptyContent = "{}";
/// <summary> /// <summary>
/// Timeout for requests /// Timeout for requests
/// </summary> /// </summary>
@ -153,11 +164,13 @@ 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="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> /// <returns></returns>
[return: NotNull] [return: NotNull]
protected virtual async Task<WebCallResult<T>> SendRequest<T>(Uri uri, HttpMethod method, CancellationToken cancellationToken, 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); log.Write(LogVerbosity.Debug, "Creating request for " + uri);
if (signed && authProvider == null) if (signed && authProvider == null)
@ -166,7 +179,7 @@ namespace CryptoExchange.Net
return new WebCallResult<T>(null, null, null, new NoApiCredentialsError()); 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) foreach (var limiter in RateLimiters)
{ {
var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, RateLimitBehaviour); var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, RateLimitBehaviour);
@ -205,16 +218,38 @@ namespace CryptoExchange.Net
var responseStream = await response.GetResponseStream().ConfigureAwait(false); var responseStream = await response.GetResponseStream().ConfigureAwait(false);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
var desResult = await Deserialize<T>(responseStream).ConfigureAwait(false); if (manualParseError)
responseStream.Close(); {
response.Close(); 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 else
{ {
using var reader = new StreamReader(responseStream); using var reader = new StreamReader(responseStream);
var data = await reader.ReadToEndAsync().ConfigureAwait(false); var data = await reader.ReadToEndAsync().ConfigureAwait(false);
log.Write(LogVerbosity.Debug, $"Error received: {data}");
responseStream.Close(); responseStream.Close();
response.Close(); response.Close();
var parseResult = ValidateJson(data); 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> /// <summary>
/// Creates a request object /// Creates a request object
/// </summary> /// </summary>
@ -250,17 +296,19 @@ namespace CryptoExchange.Net
/// <param name="method">The method of the request</param> /// <param name="method">The method of the request</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="postPosition">Where the post parameters should be placed</param>
/// <param name="arraySerialization">How array paramters should be serialized</param>
/// <returns></returns> /// <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) if (parameters == null)
parameters = new Dictionary<string, object>(); parameters = new Dictionary<string, object>();
var uriString = uri.ToString(); var uriString = uri.ToString();
if(authProvider != null) 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); uriString += "?" + parameters.CreateParamString(true, arraySerialization);
var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
@ -269,17 +317,17 @@ namespace CryptoExchange.Net
var headers = new Dictionary<string, string>(); var headers = new Dictionary<string, string>();
if (authProvider != null) if (authProvider != null)
headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed); headers = authProvider.AddAuthenticationToHeaders(uriString, method, parameters!, signed, postPosition, arraySerialization);
foreach (var header in headers) foreach (var header in headers)
request.AddHeader(header.Key, header.Value); 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) if(parameters?.Any() == true)
WriteParamBody(request, parameters, contentType); WriteParamBody(request, parameters, contentType);
else else
request.SetContent("{}", contentType); request.SetContent(requestBodyEmptyContent, contentType);
} }
return request; return request;
@ -302,7 +350,16 @@ namespace CryptoExchange.Net
{ {
var formData = HttpUtility.ParseQueryString(string.Empty); var formData = HttpUtility.ParseQueryString(string.Empty);
foreach (var kvp in parameters.OrderBy(p => p.Key)) 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(); var stringData = formData.ToString();
request.SetContent(stringData, contentType); request.SetContent(stringData, contentType);
} }

View File

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

View File

@ -136,9 +136,16 @@ namespace CryptoExchange.Net.Sockets
private void ProcessMessage(string data) private void ProcessMessage(string data)
{ {
log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} received data: " + data); log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} received data: " + data);
if (string.IsNullOrEmpty(data)) return;
var tokenData = data.ToJToken(log); var tokenData = data.ToJToken(log);
if (tokenData == null) if (tokenData == null)
return; {
data = $"\"{data}\"";
tokenData = data.ToJToken(log);
if (tokenData == null)
return;
}
var handledResponse = false; var handledResponse = false;
foreach (var pendingRequest in pendingRequests.ToList()) 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. To stop synchronizing an order book use the `Stop` method.
## Release notes ## 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 * Version 3.0.5 - 05 Feb 2020
* Added PausedActivity events on socket subscriptions * Added PausedActivity events on socket subscriptions