mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-09 08:56:13 +00:00
introduce a default empty ISymbolOrderBookEntry that is returned buy BestBid or BestAsk if called when the bid or ask lists are empty. This resolves a null reference exception seen during syncronization (specifically when connecting to Kraken). I have also introduced BestOffers which returns both bid and ask in the scope of one lock allowing the caller to be sure that nothing has changed between BestBid and BestAsk request.
This commit is contained in:
parent
99d514c647
commit
08d7022815
72
CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs
Normal file
72
CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
private class TestableSymbolOrderBook : SymbolOrderBook
|
||||||
|
{
|
||||||
|
public TestableSymbolOrderBook() : base("BTC/USD", defaultOrderBookOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Item1);
|
||||||
|
Assert.IsNotNull(symbolOrderBook.BestOffers.Item2);
|
||||||
|
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Item1.Price);
|
||||||
|
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Item1.Quantity);
|
||||||
|
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Item2.Price);
|
||||||
|
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Item2.Quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1819,6 +1819,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
|
||||||
@ -2898,5 +2903,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>
|
||||||
|
@ -127,6 +127,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 +143,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
lock (bookLock)
|
lock (bookLock)
|
||||||
return bids.FirstOrDefault().Value;
|
return bids.FirstOrDefault().Value ?? emptySymbolOrderBookEntry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +155,19 @@ 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 Tuple<ISymbolOrderBookEntry, ISymbolOrderBookEntry> BestOffers {
|
||||||
|
get {
|
||||||
|
lock (bookLock)
|
||||||
|
{
|
||||||
|
return new Tuple<ISymbolOrderBookEntry, ISymbolOrderBookEntry>(BestBid,BestAsk);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,9 +318,10 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
|
|
||||||
private void CheckBestOffersChanged(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk)
|
private void CheckBestOffersChanged(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk)
|
||||||
{
|
{
|
||||||
if (BestBid.Price != prevBestBid.Price || BestBid.Quantity != prevBestBid.Quantity ||
|
var (bestBid, bestAsk) = BestOffers;
|
||||||
BestAsk.Price != prevBestAsk.Price || BestAsk.Quantity != prevBestAsk.Quantity)
|
if (bestBid.Price != prevBestBid.Price || bestBid.Quantity != prevBestBid.Quantity ||
|
||||||
OnBestOffersChanged?.Invoke(BestBid, BestAsk);
|
bestAsk.Price != prevBestAsk.Price || bestAsk.Quantity != prevBestAsk.Quantity)
|
||||||
|
OnBestOffersChanged?.Invoke(bestBid, bestAsk);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -329,8 +350,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
CheckProcessBuffer();
|
CheckProcessBuffer();
|
||||||
var prevBestBid = BestBid;
|
var (prevBestBid, prevBestAsk) = BestOffers;
|
||||||
var prevBestAsk = BestAsk;
|
|
||||||
ProcessSingleSequenceUpdates(rangeUpdateId, bids, asks);
|
ProcessSingleSequenceUpdates(rangeUpdateId, bids, asks);
|
||||||
OnOrderBookUpdate?.Invoke(bids, asks);
|
OnOrderBookUpdate?.Invoke(bids, asks);
|
||||||
CheckBestOffersChanged(prevBestBid, prevBestAsk);
|
CheckBestOffersChanged(prevBestBid, prevBestAsk);
|
||||||
@ -366,8 +386,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
CheckProcessBuffer();
|
CheckProcessBuffer();
|
||||||
var prevBestBid = BestBid;
|
var (prevBestBid, prevBestAsk) = BestOffers;
|
||||||
var prevBestAsk = BestAsk;
|
|
||||||
ProcessRangeUpdates(firstUpdateId, lastUpdateId, bids, asks);
|
ProcessRangeUpdates(firstUpdateId, lastUpdateId, bids, asks);
|
||||||
OnOrderBookUpdate?.Invoke(bids, asks);
|
OnOrderBookUpdate?.Invoke(bids, asks);
|
||||||
CheckBestOffersChanged(prevBestBid, prevBestAsk);
|
CheckBestOffersChanged(prevBestBid, prevBestAsk);
|
||||||
@ -396,8 +415,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
CheckProcessBuffer();
|
CheckProcessBuffer();
|
||||||
var prevBestBid = BestBid;
|
var (prevBestBid, prevBestAsk) = BestOffers;
|
||||||
var prevBestAsk = BestAsk;
|
|
||||||
ProcessUpdates(bids, asks);
|
ProcessUpdates(bids, asks);
|
||||||
OnOrderBookUpdate?.Invoke(bids, asks);
|
OnOrderBookUpdate?.Invoke(bids, asks);
|
||||||
CheckBestOffersChanged(prevBestBid, prevBestAsk);
|
CheckBestOffersChanged(prevBestBid, prevBestAsk);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user