1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-10-27 00:17:31 +00:00
Jkorf c22b54c898 Squashed commit of the following:
commit 9450d447b9822470504e3031e57a65146c838e0e
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Feb 18 11:05:46 2022 +0100

    Updated version

commit bc0b55f3372f32bf7dd6947f4ea4f02f1bfeaa05
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Feb 18 10:09:26 2022 +0100

    Added clientOrderId parameter to common clients

commit 31111006c728d4d1b513c32838ca5b92e33a4c4a
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Feb 17 16:32:53 2022 +0100

    Update SpotClient.razor

commit e7400ce334175961426daffd6827e08349e518b6
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Feb 17 16:10:49 2022 +0100

    Made some names more generic

commit 9bdef400daaed68d48f4be2c0a3311498bac5b1c
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Feb 15 11:38:41 2022 +0100

    Updated vesrion

commit 3b80a945eef9c42de8b19850b2e0fe45f2d6caa0
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Feb 15 11:34:50 2022 +0100

    docs

commit 0268e211e90956016652280c6d2b9b7ec4c4e701
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Feb 15 09:56:45 2022 +0100

    Immediate initial reconnect attempt when connection is lost

commit 6eb43c5218fcaab2e51538a93776d538f9b9e7fc
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Feb 11 13:59:05 2022 +0100

    Re-added recalculation interval

commit 1df63ab60c5e0f63f64d16a07ae452dd6bd92ee3
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Feb 9 14:32:00 2022 +0100

    Updated version

commit 9461b57daa9ae4d74702b38de15cf2ee8c461263
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Feb 9 13:37:12 2022 +0100

    Fix for time offset calculation not updating when offset is < 500ms

commit 105547d6b16d99258adb96c150bef7ffbc82b487
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Feb 5 21:05:10 2022 +0100

    Updated version

commit 379ded6832d25ada47519f979c43c05daaf4d17c
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Feb 5 20:29:57 2022 +0100

    Fixed tests

commit b18204a52d8c26650059fc88631ad9eccc505f15
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Feb 5 20:28:08 2022 +0100

    Added CancellationToken support on Common client interface and SymbolOrderBook, improved SymbolOrderBook start/stop robustness

commit baa23c2eccb6f84c875be3c60e77c395e8b7cd90
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Feb 5 14:56:32 2022 +0100

    Added GetSubscriptionByRequest method on socket connection

commit 7aad9482a540865c4f83bea7aaf763979e02cdba
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Feb 2 10:57:06 2022 +0100

    Updated version

commit 6e4d9d225eb4076a3c2c6586c5a4cec391e04d00
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Feb 2 09:42:22 2022 +0100

    Fixed exception when deserializing non-nullable datetime value '0' in .net framework

commit fd1a2bbda95314f4b03d1d0e0078171238429c7c
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 25 13:19:10 2022 +0100

    Updated version

commit 2ece04dd58f7524e4d50447fe1813d1c3ef7e5d4
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 25 13:17:25 2022 +0100

    Refactored use of AutoResetEvent to AsyncResetEvent in SymbolOrderBook

commit 893d0c723d55c026ab854d0945a03186828d59b3
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 25 13:01:21 2022 +0100

    Fixed DateTime converter for nanosecond times in string format

commit 2c43ee7554af43adee40082b4500bc1e9c041d36
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 24 15:56:24 2022 +0100

    Updated version version; fixed dependencies

commit 100a34d1a0372940dd6f02599c46306fed263c6c
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 24 14:37:15 2022 +0100

    Updated version

commit bb1071472f4170c2f250e8c3775e888b02b7a1ed
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 24 14:31:57 2022 +0100

    Re-added Common prefix for common enums to avoid conflicts with library namespaces

commit 37b1d18104851797e3bc617c976ad2962f183b90
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jan 21 15:25:33 2022 +0100

    Updated version

commit 325389cdf81e51ef829bb2c5edd45cd4adc00d09
Author: Jan Korf <jankorf91@gmail.com>
Date:   Thu Jan 20 21:08:51 2022 +0100

    Added FTX to console example

commit 3e23882572e42b54e30c0727f4002a8c3d620b88
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Jan 20 16:21:42 2022 +0100

    Replaced Debug.WriteLine with Trace.WriteLine

commit 3cf5480cad23db99711486afbdb71e242f65de76
Author: Jan Korf <jankorf91@gmail.com>
Date:   Wed Jan 19 22:07:22 2022 +0100

    Example

commit fe31cf156d76c41159ff4ffee02c720929a2aaa4
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Jan 19 16:35:08 2022 +0100

    Examples

commit 7427914cb76cc44dda2c095e90bf89a04cf802d0
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 16:46:43 2022 +0100

    Update index.md

commit 1bc62258140d4f11c3348ea6f32fc81ff67e0186
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 16:45:10 2022 +0100

    docs

commit 259fe6bfd12026161aa6bdb167e0a9da3e5eca6e
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 14:25:20 2022 +0100

    Update index.md

commit 5f9c075ac7fc8ae1d35b71ea8db99168c2945511
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 14:22:33 2022 +0100

    Update index.md

commit a26514016a0cebb86117be25987e425e47bfb2f9
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 14:13:35 2022 +0100

    Update index.md

commit 01a97412bffcbded0ade5406a0e482236fa6f3f4
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 14:12:02 2022 +0100

    docs

commit 24b503ca8cdeb7d1ab98467a38b6fc4905d53246
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 13:42:12 2022 +0100

    docs

commit 008b15b055bf6c4793f0ca26bca8a302f0b1614a
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Jan 18 13:32:55 2022 +0100

    docs

commit 66fce6cb849ae86b0b71bdb625c2b4f905a0ba33
Author: Jan Korf <jankorf91@gmail.com>
Date:   Mon Jan 17 21:31:53 2022 +0100

    docs

commit 0f65701f902bfb290d4589d05debc2de4b6d5705
Author: Jan Korf <jankorf91@gmail.com>
Date:   Mon Jan 17 21:25:33 2022 +0100

    docs

commit f7a405a2e6e518ff1d9bef0a43ac0e0759aea1d2
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 16:32:50 2022 +0100

    docs

commit 55284c0549a38ae88cc7fe0c9f85fe8326bb1f3d
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 15:51:03 2022 +0100

    docs

commit 5bfbcca25bf84ab429007555c9b87331d867172f
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 14:04:08 2022 +0100

    docs

commit cdbc0ba215cb978b0bc3e3d2fe23b874b3c07256
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 13:58:51 2022 +0100

    docs

commit e33e7c6775b9a355bdaf38a407bd3d4c09809ccb
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 13:47:58 2022 +0100

    docs

commit b65669659d189066d0ef6e19ff9c02fb27cd18f1
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 13:44:45 2022 +0100

    docs

commit e51b8632424965e25cee5f70386aed9b20601255
Merge: dbfe34f 088f35d
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 13:36:51 2022 +0100

    Merge branch 'feature/new-cc' of https://github.com/JKorf/CryptoExchange.Net into feature/new-cc

commit dbfe34f53449c1d84948500d36fc0af7278befa9
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 17 13:35:46 2022 +0100

    Docs

commit 088f35d42099a60c8a820c603507b187f4038460
Author: Jan Korf <jankorf91@gmail.com>
Date:   Mon Jan 17 13:34:40 2022 +0100

    Set theme jekyll-theme-cayman

commit e77add4d1c84ccf1a7e8b55859883be36d72bdc3
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Jan 15 15:26:38 2022 +0100

    Updated version

commit a37a2d6e31e3bbf13ed745761bc75c17638bddc2
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Jan 15 15:23:52 2022 +0100

    Added CallResult tests, fixed response time not set

commit 8f6e853e13756260b78ff3b718ea5e6d200bab3e
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jan 14 16:47:49 2022 +0100

    Added Request info and ResponseTime to WebCallResult, refactored CallResult ctors

commit c6bf0d67a45ae85f3b022b1257f4d9eeb322fa8e
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jan 7 16:30:02 2022 +0100

    Fix typo

commit 996f3c2ced8caa8022f3e8b4e3c166b345a98842
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jan 7 16:23:42 2022 +0100

    Some options logging

commit fb9e9f9aa65b0fdd5866387316e9277f218482f1
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jan 7 15:10:27 2022 +0100

    Updated version

commit 52ebacaa212b1c663cdf7433876776f350692f3e
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Jan 7 15:05:51 2022 +0100

    Fixed symbol order book tostring not locking thread, Potential fix for request timeout showing unclear message

commit 6b4585993450daba95b84292fdc0d73f0f9dc7b7
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 3 14:08:35 2022 +0100

    Updated example

commit ebe332b724fbe4f7c9e2b2bf4864049e7dfa31d6
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 3 12:05:47 2022 +0100

    Updated version

commit 8c24b46fb32408afacea86c9504f4a463fae3c75
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 3 11:33:07 2022 +0100

    Fixed typo Comon -> Common

commit 7a195f662c6c339d7d69e88025803f497f1ae30d
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Jan 3 09:37:50 2022 +0100

    Updated example, removed global.json

commit 120132c45b9b8cc7703d0b9a9bbdba1f54e92f9b
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Jan 1 20:30:35 2022 +0100

    Reverted conditional refs

commit b3b4ed3f3fd0fd0fa536e1fa245c05fdb65633e0
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Jan 1 19:45:59 2022 +0100

    Updated version

commit f4b4c93e6473961875f114b47d3434714f57c8b9
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sat Jan 1 19:40:50 2022 +0100

    Added new shared interface implementation

commit f8c3b37cdf3baa42715cf5b31e5884fd9f077db4
Author: Jan Korf <jankorf91@gmail.com>
Date:   Tue Dec 28 14:14:15 2021 +0100

    wip example

commit 0117737dfacb8f37f087ab8d59ae806ab66b1e55
Author: Jan Korf <jankorf91@gmail.com>
Date:   Tue Dec 28 14:13:14 2021 +0100

    Added conditional refs for Microsoft.Extensions, added DependencyInjection.Abstractions to support extension method on IServiceCollection

commit 02c1f874e17a9fae1579ed2ce5e8a6db99377738
Author: Jan Korf <jankorf91@gmail.com>
Date:   Mon Dec 27 15:32:07 2021 +0100

    Updated version

commit b212842ec8048be584ae034c9cf2c28a97ecfa3e
Author: Jan Korf <jankorf91@gmail.com>
Date:   Mon Dec 27 15:27:14 2021 +0100

    Added ExchangeName to IExchangeClient interface

commit c96e75d6c3ef0e28a492755b0c2bf2cc2531f4e1
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Dec 21 16:22:51 2021 +0100

    Updated version

commit c62fbda3d74c00c11e030f250a91494ab4b538d9
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Dec 17 14:17:30 2021 +0100

    Added ApiClients list for managing api credentials, requests made and dispose

commit 04b43257a549666d793674931640810c8f276c1e
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Dec 16 16:17:26 2021 +0100

    Update .gitignore

commit 8ba0ded16d12a7fadffe87d7819a7ddd9df457bc
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Dec 13 12:57:31 2021 +0100

    Fixed api credentials getting disposed, fixed DateTimeConverter losing precision

commit 5c665ad54ca40e473b236ddacf54aa9da563320e
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Dec 10 16:35:42 2021 +0100

    Refactoring and comments

commit b7cd6a866acf4d91b48d4e50216f6627a299c3a7
Author: Jan Korf <jankorf91@gmail.com>
Date:   Wed Dec 8 21:49:25 2021 +0100

    Auth work

commit c2105fe690c6b4a468d3d45e847a2e32172c702a
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Dec 8 16:20:44 2021 +0100

    Wip, support for time syncing, refactoring authentication

commit 8b479547ab7330a143502eccb51f0f38e88fe4b9
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Dec 7 15:47:55 2021 +0100

    Fixed release name

commit 2ab032b8718d6dcfbe0904eb5d8ad2cb6c4d2889
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Dec 7 15:47:14 2021 +0100

    Updated version

commit 48baaeb2d8579e4844aaeb3aac30017a71dcb460
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Dec 6 16:18:18 2021 +0100

    Added periodic identifier

commit 60ec18919a080f004bfa1f35dc604d372fc837d9
Author: Jan Korf <jankorf91@gmail.com>
Date:   Sun Dec 5 17:26:55 2021 +0100

    Added quotes to log

commit 0818c6277b10f0b334de3145318ac1f6fb1596a8
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Dec 3 16:23:05 2021 +0100

    Small changes

commit 6d0120d564183984d77574921b401127934e43c7
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Dec 1 16:26:34 2021 +0100

    Comments, fix test

commit 3c3b5639f59e07fb5c70d120c15a247d32dfab98
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Dec 1 13:31:54 2021 +0100

    Refactor clients/options

commit 49de7e89ccf6e16dee3ffb10ca77f2f0e2720ac2
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Nov 30 10:31:45 2021 +0100

    Disposable changes, fixed tests

commit 69a6fabb790770b4302e8eb26f9e4acd62cc868d
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Nov 29 16:43:27 2021 +0100

    Restruct

commit 9a266e44ced9d9f887fe9e664c1ca393ca008008
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Nov 26 09:32:26 2021 +0100

    Added enum converter

commit 78f81393a441ca34d067a20973e3410761cbf77a
Author: Jkorf <jankorf91@gmail.com>
Date:   Thu Nov 25 10:25:56 2021 +0100

    Removed old timestamp converters

commit 9ebe5de825ed81a4fe77563d602882f7e9847352
Author: Jan Korf <jankorf91@gmail.com>
Date:   Wed Nov 24 19:32:37 2021 +0100

    Added AppendPath method

commit 8b619e82f2953c88e15c1a52e3a09b8de495dfed
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Nov 24 16:39:14 2021 +0100

    Added DateTimeConverter as replacement for individual converters, fix for not closing socket when auth fails

commit 7ac7a11dfe87f1ad9b06eaf1327e334f255e0477
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Nov 17 10:23:01 2021 +0100

    Resolved some code issues

commit 3784b0c62b2e0ddba3018fbf18340ee20c32f879
Author: Jkorf <jankorf91@gmail.com>
Date:   Mon Nov 15 16:36:30 2021 +0100

    Ratelimiter rework

commit cb1826da7acf730e32bbad43458662ca4e25f35a
Author: Jkorf <jankorf91@gmail.com>
Date:   Fri Nov 12 09:40:42 2021 +0100

    Documentation

commit f7445543f261d517bfafa4657eba0e4f7b013da7
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Nov 10 16:44:46 2021 +0100

    Exposed order book id

commit 6c3462403f25382365ff8633f62bf8ca3194d343
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Nov 10 13:18:52 2021 +0100

    Fixed tests

commit f83127590ac0b2fd0a9258c21458a05d714a1d14
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Nov 3 08:27:03 2021 +0100

    wip

commit 23bbf0ef8869591e55d9e6822360d9edd9ef6c92
Author: Jkorf <jankorf91@gmail.com>
Date:   Wed Oct 27 12:57:23 2021 +0200

    Added cancellation token support for socket subscriptions

commit b7f1619aec8c09b93777bd6319c3adbc8216927b
Merge: 6ce6a46 f6af235
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Oct 26 15:39:52 2021 +0200

    Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net

commit 6ce6a46ca347468c23e21f561de41ec3fce51e3f
Author: Jkorf <jankorf91@gmail.com>
Date:   Tue Oct 26 15:39:50 2021 +0200

    Some renames
2022-02-18 11:06:34 +01:00

762 lines
29 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.OrderBook
{
/// <summary>
/// Base for order book implementations
/// </summary>
public abstract class SymbolOrderBook : ISymbolOrderBook, IDisposable
{
private readonly object _bookLock = new object();
private OrderBookStatus _status;
private UpdateSubscription? _subscription;
private bool _stopProcessing;
private Task? _processTask;
private CancellationTokenSource? _cts;
private readonly AsyncResetEvent _queueEvent;
private readonly ConcurrentQueue<object> _processQueue;
private readonly bool _validateChecksum;
private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry
{
public decimal Quantity { get => 0m;
set { } }
public decimal Price { get => 0m;
set { } }
}
private static readonly ISymbolOrderBookEntry emptySymbolOrderBookEntry = new EmptySymbolOrderBookEntry();
/// <summary>
/// A buffer to store messages received before the initial book snapshot is processed. These messages
/// will be processed after the book snapshot is set. Any messages in this buffer with sequence numbers lower
/// than the snapshot sequence number will be discarded
/// </summary>
protected readonly List<ProcessBufferRangeSequenceEntry> processBuffer;
/// <summary>
/// The ask list, should only be accessed using the bookLock
/// </summary>
protected SortedList<decimal, ISymbolOrderBookEntry> asks;
/// <summary>
/// The bid list, should only be accessed using the bookLock
/// </summary>
protected SortedList<decimal, ISymbolOrderBookEntry> bids;
/// <summary>
/// The log
/// </summary>
protected Log log;
/// <summary>
/// Whether update numbers are consecutive. If set to true and an update comes in which isn't the previous sequences number + 1
/// the book will resynchronize as it is deemed out of sync
/// </summary>
protected bool sequencesAreConsecutive;
/// <summary>
/// Whether levels should be strictly enforced. For example, when an order book has 25 levels and a new update comes in which pushes
/// the current level 25 ask out of the top 25, should the curent the level 26 entry be removed from the book or does the
/// server handle this
/// </summary>
protected bool strictLevels;
/// <summary>
/// If the initial snapshot of the book has been set
/// </summary>
protected bool bookSet;
/// <summary>
/// The amount of levels for this book
/// </summary>
protected int? Levels { get; set; } = null;
/// <inheritdoc/>
public string Id { get; }
/// <inheritdoc/>
public OrderBookStatus Status
{
get => _status;
set
{
if (value == _status)
return;
var old = _status;
_status = value;
log.Write(LogLevel.Information, $"{Id} order book {Symbol} status changed: {old} => {value}");
OnStatusChange?.Invoke(old, _status);
}
}
/// <inheritdoc/>
public long LastSequenceNumber { get; private set; }
/// <inheritdoc/>
public string Symbol { get; }
/// <inheritdoc/>
public event Action<OrderBookStatus, OrderBookStatus>? OnStatusChange;
/// <inheritdoc/>
public event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)>? OnBestOffersChanged;
/// <inheritdoc/>
public event Action<(IEnumerable<ISymbolOrderBookEntry> Bids, IEnumerable<ISymbolOrderBookEntry> Asks)>? OnOrderBookUpdate;
/// <inheritdoc/>
public DateTime UpdateTime { get; private set; }
/// <inheritdoc/>
public int AskCount { get; private set; }
/// <inheritdoc/>
public int BidCount { get; private set; }
/// <inheritdoc/>
public IEnumerable<ISymbolOrderBookEntry> Asks
{
get
{
lock (_bookLock)
return asks.Select(a => a.Value).ToList();
}
}
/// <inheritdoc/>
public IEnumerable<ISymbolOrderBookEntry> Bids
{
get
{
lock (_bookLock)
return bids.Select(a => a.Value).ToList();
}
}
/// <inheritdoc/>
public (IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks) Book
{
get
{
lock (_bookLock)
return (Bids, Asks);
}
}
/// <inheritdoc/>
public ISymbolOrderBookEntry BestBid
{
get
{
lock (_bookLock)
return bids.FirstOrDefault().Value ?? emptySymbolOrderBookEntry;
}
}
/// <inheritdoc/>
public ISymbolOrderBookEntry BestAsk
{
get
{
lock (_bookLock)
return asks.FirstOrDefault().Value ?? emptySymbolOrderBookEntry;
}
}
/// <inheritdoc/>
public (ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers {
get {
lock (_bookLock)
return (BestBid,BestAsk);
}
}
/// <summary>
/// ctor
/// </summary>
/// <param name="id">The id of the order book. Should be set to {Exchange}[{type}], for example: Kucoin[Spot]</param>
/// <param name="symbol">The symbol the order book is for</param>
/// <param name="options">The options for the order book</param>
protected SymbolOrderBook(string id, string symbol, OrderBookOptions options)
{
if (symbol == null)
throw new ArgumentNullException(nameof(symbol));
if (options == null)
throw new ArgumentNullException(nameof(options));
Id = id;
processBuffer = new List<ProcessBufferRangeSequenceEntry>();
_processQueue = new ConcurrentQueue<object>();
_queueEvent = new AsyncResetEvent(false, true);
_validateChecksum = options.ChecksumValidationEnabled;
Symbol = symbol;
Status = OrderBookStatus.Disconnected;
asks = new SortedList<decimal, ISymbolOrderBookEntry>();
bids = new SortedList<decimal, ISymbolOrderBookEntry>(new DescComparer<decimal>());
log = new Log(id) { Level = options.LogLevel };
var writers = options.LogWriters ?? new List<ILogger> { new DebugLogger() };
log.UpdateWriters(writers.ToList());
}
/// <inheritdoc/>
public async Task<CallResult<bool>> StartAsync(CancellationToken? ct = null)
{
if (Status != OrderBookStatus.Disconnected)
throw new InvalidOperationException($"Can't start book unless state is {OrderBookStatus.Connecting}. Was {Status}");
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} starting");
_cts = new CancellationTokenSource();
ct?.Register(async () =>
{
_cts.Cancel();
await StopAsync().ConfigureAwait(false);
}, false);
// Clear any previous messages
while (_processQueue.TryDequeue(out _)) { }
processBuffer.Clear();
bookSet = false;
Status = OrderBookStatus.Connecting;
_processTask = Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
var startResult = await DoStartAsync(_cts.Token).ConfigureAwait(false);
if (!startResult)
{
Status = OrderBookStatus.Disconnected;
return new CallResult<bool>(startResult.Error!);
}
if (_cts.IsCancellationRequested)
{
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopped while starting");
await startResult.Data.CloseAsync().ConfigureAwait(false);
Status = OrderBookStatus.Disconnected;
return new CallResult<bool>(new CancellationRequestedError());
}
_subscription = startResult.Data;
_subscription.ConnectionLost += () =>
{
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost");
Status = OrderBookStatus.Reconnecting;
Reset();
};
_subscription.ConnectionClosed += () =>
{
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} disconnected");
Status = OrderBookStatus.Disconnected;
_ = StopAsync();
};
_subscription.ConnectionRestored += async time => await ResyncAsync().ConfigureAwait(false);
Status = OrderBookStatus.Synced;
return new CallResult<bool>(true);
}
/// <inheritdoc/>
public async Task StopAsync()
{
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopping");
Status = OrderBookStatus.Disconnected;
_cts?.Cancel();
_queueEvent.Set();
if (_processTask != null)
await _processTask.ConfigureAwait(false);
if (_subscription != null)
await _subscription.CloseAsync().ConfigureAwait(false);
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopped");
}
/// <inheritdoc/>
public CallResult<decimal> CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type)
{
if (Status != OrderBookStatus.Synced)
return new CallResult<decimal>(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state"));
var totalCost = 0m;
var totalAmount = 0m;
var amountLeft = quantity;
lock (_bookLock)
{
var list = type == OrderBookEntryType.Ask ? asks : bids;
var step = 0;
while (amountLeft > 0)
{
if (step == list.Count)
return new CallResult<decimal>(new InvalidOperationError("Quantity is larger than order in the order book"));
var element = list.ElementAt(step);
var stepAmount = Math.Min(element.Value.Quantity, amountLeft);
totalCost += stepAmount * element.Value.Price;
totalAmount += stepAmount;
amountLeft -= stepAmount;
step++;
}
}
return new CallResult<decimal>(Math.Round(totalCost / totalAmount, 8));
}
/// <summary>
/// Implementation for starting the order book. Should typically have logic for subscribing to the update stream and retrieving
/// and setting the initial order book
/// </summary>
/// <returns></returns>
protected abstract Task<CallResult<UpdateSubscription>> DoStartAsync(CancellationToken ct);
/// <summary>
/// Reset the order book
/// </summary>
protected virtual void DoReset() { }
/// <summary>
/// Resync the order book
/// </summary>
/// <returns></returns>
protected abstract Task<CallResult<bool>> DoResyncAsync(CancellationToken ct);
/// <summary>
/// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false)
/// the order book will be resynchronized
/// </summary>
/// <param name="checksum"></param>
/// <returns></returns>
protected virtual bool DoChecksum(int checksum) => true;
/// <summary>
/// Set the initial data for the order book. Typically the snapshot which was requested from the Rest API, or the first snapshot
/// received from a socket subcription
/// </summary>
/// <param name="orderBookSequenceNumber">The last update sequence number until which the snapshot is in sync</param>
/// <param name="askList">List of asks</param>
/// <param name="bidList">List of bids</param>
protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable<ISymbolOrderBookEntry> bidList, IEnumerable<ISymbolOrderBookEntry> askList)
{
_processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList });
_queueEvent.Set();
}
/// <summary>
/// Add an update to the process queue. Updates the book by providing changed bids and asks, along with an update number which should be higher than the previous update numbers
/// </summary>
/// <param name="updateId">The sequence number</param>
/// <param name="bids">List of updated/new bids</param>
/// <param name="asks">List of updated/new asks</param>
protected void UpdateOrderBook(long updateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = updateId, EndUpdateId = updateId, Asks = asks, Bids = bids });
_queueEvent.Set();
}
/// <summary>
/// Add an update to the process queue. Updates the book by providing changed bids and asks, along with the first and last sequence number in the update
/// </summary>
/// <param name="firstUpdateId">The sequence number of the first update</param>
/// <param name="lastUpdateId">The sequence number of the last update</param>
/// <param name="bids">List of updated/new bids</param>
/// <param name="asks">List of updated/new asks</param>
protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids });
_queueEvent.Set();
}
/// <summary>
/// Add an update to the process queue. Updates the book by providing changed bids and asks, each with its own sequence number
/// </summary>
/// <param name="bids">List of updated/new bids</param>
/// <param name="asks">List of updated/new asks</param>
protected void UpdateOrderBook(IEnumerable<ISymbolOrderSequencedBookEntry> bids, IEnumerable<ISymbolOrderSequencedBookEntry> asks)
{
var highest = Math.Max(bids.Any() ? bids.Max(b => b.Sequence) : 0, asks.Any() ? asks.Max(a => a.Sequence) : 0);
var lowest = Math.Min(bids.Any() ? bids.Min(b => b.Sequence) : long.MaxValue, asks.Any() ? asks.Min(a => a.Sequence) : long.MaxValue);
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = lowest, EndUpdateId = highest, Asks = asks, Bids = bids });
_queueEvent.Set();
}
/// <summary>
/// Add a checksum value to the process queue
/// </summary>
/// <param name="checksum">The checksum value</param>
protected void AddChecksum(int checksum)
{
_processQueue.Enqueue(new ChecksumItem() { Checksum = checksum });
_queueEvent.Set();
}
/// <summary>
/// Check and empty the process buffer; see what entries to update the book with
/// </summary>
protected void CheckProcessBuffer()
{
var pbList = processBuffer.ToList();
if (pbList.Count > 0)
log.Write(LogLevel.Debug, "Processing buffered updates");
foreach (var bufferEntry in pbList)
{
ProcessRangeUpdates(bufferEntry.FirstUpdateId, bufferEntry.LastUpdateId, bufferEntry.Bids, bufferEntry.Asks);
processBuffer.Remove(bufferEntry);
}
}
/// <summary>
/// Update order book with an entry
/// </summary>
/// <param name="sequence">Sequence number of the update</param>
/// <param name="type">Type of entry</param>
/// <param name="entry">The entry</param>
protected virtual bool ProcessUpdate(long sequence, OrderBookEntryType type, ISymbolOrderBookEntry entry)
{
if (sequence <= LastSequenceNumber)
{
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}");
return false;
}
if (sequencesAreConsecutive && sequence > LastSequenceNumber + 1)
{
// Out of sync
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting");
_stopProcessing = true;
Resubscribe();
return false;
}
UpdateTime = DateTime.UtcNow;
var listToChange = type == OrderBookEntryType.Ask ? asks : bids;
if (entry.Quantity == 0)
{
if (!listToChange.ContainsKey(entry.Price))
return true;
listToChange.Remove(entry.Price);
if (type == OrderBookEntryType.Ask) AskCount--;
else BidCount--;
}
else
{
if (!listToChange.ContainsKey(entry.Price))
{
listToChange.Add(entry.Price, entry);
if (type == OrderBookEntryType.Ask) AskCount++;
else BidCount++;
}
else
{
listToChange[entry.Price] = entry;
}
}
return true;
}
/// <summary>
/// Wait until the order book snapshot has been set
/// </summary>
/// <param name="timeout">Max wait time</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected async Task<CallResult<bool>> WaitForSetOrderBookAsync(int timeout, CancellationToken ct)
{
var startWait = DateTime.UtcNow;
while (!bookSet && Status == OrderBookStatus.Syncing)
{
if(ct.IsCancellationRequested)
return new CallResult<bool>(new CancellationRequestedError());
if ((DateTime.UtcNow - startWait).TotalMilliseconds > timeout)
return new CallResult<bool>(new ServerError("Timeout while waiting for data"));
try
{
await Task.Delay(10, ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{ }
}
return new CallResult<bool>(true);
}
/// <summary>
/// Dispose the order book
/// </summary>
public abstract void Dispose();
/// <summary>
/// String representation of the top 3 entries
/// </summary>
/// <returns></returns>
public override string ToString()
{
return ToString(3);
}
/// <summary>
/// String representation of the top x entries
/// </summary>
/// <returns></returns>
public string ToString(int numberOfEntries)
{
var stringBuilder = new StringBuilder();
var book = Book;
stringBuilder.AppendLine($" Ask quantity Ask price | Bid price Bid quantity");
for(var i = 0; i < numberOfEntries; i++)
{
var ask = book.asks.Count() > i ? book.asks.ElementAt(i): null;
var bid = book.bids.Count() > i ? book.bids.ElementAt(i): null;
stringBuilder.AppendLine($"[{ask?.Quantity.ToString(CultureInfo.InvariantCulture),14}] {ask?.Price.ToString(CultureInfo.InvariantCulture),14} | {bid?.Price.ToString(CultureInfo.InvariantCulture),-14} [{bid?.Quantity.ToString(CultureInfo.InvariantCulture),-14}]");
}
return stringBuilder.ToString();
}
private void CheckBestOffersChanged(ISymbolOrderBookEntry prevBestBid, ISymbolOrderBookEntry prevBestAsk)
{
var (bestBid, bestAsk) = BestOffers;
if (bestBid.Price != prevBestBid.Price || bestBid.Quantity != prevBestBid.Quantity ||
bestAsk.Price != prevBestAsk.Price || bestAsk.Quantity != prevBestAsk.Quantity)
OnBestOffersChanged?.Invoke((bestBid, bestAsk));
}
private void Reset()
{
_queueEvent.Set();
// Clear queue
while (_processQueue.TryDequeue(out _)) { }
processBuffer.Clear();
bookSet = false;
DoReset();
}
private async Task ResyncAsync()
{
Status = OrderBookStatus.Syncing;
var success = false;
while (!success)
{
if (Status != OrderBookStatus.Syncing)
return;
var resyncResult = await DoResyncAsync(_cts!.Token).ConfigureAwait(false);
success = resyncResult;
}
log.Write(LogLevel.Information, $"{Id} order book {Symbol} successfully resynchronized");
Status = OrderBookStatus.Synced;
}
private async Task ProcessQueue()
{
while (Status != OrderBookStatus.Disconnected)
{
await _queueEvent.WaitAsync().ConfigureAwait(false);
while (_processQueue.TryDequeue(out var item))
{
if (Status == OrderBookStatus.Disconnected)
break;
if (_stopProcessing)
{
log.Write(LogLevel.Trace, "Skipping message because of resubscribing");
continue;
}
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)
{
bookSet = true;
asks.Clear();
foreach (var ask in item.Asks)
asks.Add(ask.Price, ask);
bids.Clear();
foreach (var bid in item.Bids)
bids.Add(bid.Price, bid);
LastSequenceNumber = item.EndUpdateId;
AskCount = asks.Count;
BidCount = bids.Count;
UpdateTime = DateTime.UtcNow;
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}");
CheckProcessBuffer();
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
}
}
private void ProcessQueueItem(ProcessQueueItem item)
{
lock (_bookLock)
{
if (!bookSet)
{
processBuffer.Add(new ProcessBufferRangeSequenceEntry()
{
Asks = item.Asks,
Bids = item.Bids,
FirstUpdateId = item.StartUpdateId,
LastUpdateId = item.EndUpdateId,
});
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]");
}
else
{
CheckProcessBuffer();
var (prevBestBid, prevBestAsk) = BestOffers;
ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks);
if (!asks.Any() || !bids.Any())
return;
if (asks.First().Key < bids.First().Key)
{
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} detected out of sync order book. First ask: {asks.First().Key}, first bid: {bids.First().Key}. Resyncing");
_stopProcessing = true;
Resubscribe();
return;
}
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
CheckBestOffersChanged(prevBestBid, prevBestAsk);
}
}
}
private void ProcessChecksum(ChecksumItem ci)
{
lock (_bookLock)
{
if (!_validateChecksum)
return;
bool checksumResult = false;
try
{
checksumResult = DoChecksum(ci.Checksum);
}
catch (Exception)
{
// If the status is not synced it can be expected a checksum is failing
if (Status == OrderBookStatus.Synced)
throw;
}
if (!checksumResult)
{
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync. Resyncing");
_stopProcessing = true;
Resubscribe();
}
}
}
private void Resubscribe()
{
Status = OrderBookStatus.Syncing;
_ = Task.Run(async () =>
{
if(_subscription == null)
{
Status = OrderBookStatus.Disconnected;
return;
}
await _subscription!.UnsubscribeAsync().ConfigureAwait(false);
Reset();
_stopProcessing = false;
if (!await _subscription!.ResubscribeAsync().ConfigureAwait(false))
{
// Resubscribing failed, reconnect the socket
log.Write(LogLevel.Warning, $"{Id} order book {Symbol} resync failed, reconnecting socket");
Status = OrderBookStatus.Reconnecting;
_ = _subscription!.ReconnectAsync();
}
else
await ResyncAsync().ConfigureAwait(false);
});
}
private void ProcessRangeUpdates(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
{
if (lastUpdateId <= LastSequenceNumber)
{
log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}");
return;
}
foreach (var entry in bids)
ProcessUpdate(LastSequenceNumber + 1, OrderBookEntryType.Bid, entry);
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(LogLevel.Debug, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}");
}
}
internal class DescComparer<T> : IComparer<T>
{
public int Compare(T x, T y)
{
return Comparer<T>.Default.Compare(y, x);
}
}
}