diff --git a/CryptoExchange.Net/Interfaces/ISpotClient.cs b/CryptoExchange.Net/Interfaces/ISpotClient.cs index a64f33c..6293e49 100644 --- a/CryptoExchange.Net/Interfaces/ISpotClient.cs +++ b/CryptoExchange.Net/Interfaces/ISpotClient.cs @@ -2,6 +2,7 @@ using CryptoExchange.Net.Objects; using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace CryptoExchange.Net.Interfaces @@ -28,29 +29,32 @@ namespace CryptoExchange.Net.Interfaces /// /// Get the symbol name based on a base and quote asset /// - /// - /// + /// The base asset + /// The quote asset /// string GetSymbolName(string baseAsset, string quoteAsset); /// /// Get a list of symbols for the exchange /// + /// [Optional] Cancellation token for cancelling the request /// - Task>> GetSymbolsAsync(); + Task>> GetSymbolsAsync(CancellationToken ct = default); /// /// Get a ticker for the exchange /// /// The symbol to get klines for + /// [Optional] Cancellation token for cancelling the request /// - Task> GetTickerAsync(string symbol); + Task> GetTickerAsync(string symbol, CancellationToken ct = default); /// /// Get a list of tickers for the exchange /// + /// [Optional] Cancellation token for cancelling the request /// - Task>> GetTickersAsync(); + Task>> GetTickersAsync(CancellationToken ct = default); /// /// Get a list of candles for a given symbol on the exchange @@ -60,67 +64,76 @@ namespace CryptoExchange.Net.Interfaces /// [Optional] Start time to retrieve klines for /// [Optional] End time to retrieve klines for /// [Optional] Max number of results + /// [Optional] Cancellation token for cancelling the request /// - Task>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null); + Task>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, CancellationToken ct = default); /// /// Get the order book for a symbol /// /// The symbol to get the book for + /// [Optional] Cancellation token for cancelling the request /// - Task> GetOrderBookAsync(string symbol); + Task> GetOrderBookAsync(string symbol, CancellationToken ct = default); /// /// The recent trades for a symbol /// /// The symbol to get the trades for + /// [Optional] Cancellation token for cancelling the request /// - Task>> GetRecentTradesAsync(string symbol); + Task>> GetRecentTradesAsync(string symbol, CancellationToken ct = default); /// /// Get balances /// /// [Optional] The account id to retrieve balances for, required for some exchanges, ignored otherwise + /// [Optional] Cancellation token for cancelling the request /// - Task>> GetBalancesAsync(string? accountId = null); + Task>> GetBalancesAsync(string? accountId = null, CancellationToken ct = default); /// /// Get an order by id /// /// The id /// [Optional] The symbol the order is on, required for some exchanges, ignored otherwise + /// [Optional] Cancellation token for cancelling the request /// - Task> GetOrderAsync(string orderId, string? symbol = null); + Task> GetOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default); /// /// Get trades for an order by id /// /// The id /// [Optional] The symbol the order is on, required for some exchanges, ignored otherwise + /// [Optional] Cancellation token for cancelling the request /// - Task>> GetOrderTradesAsync(string orderId, string? symbol = null); + Task>> GetOrderTradesAsync(string orderId, string? symbol = null, CancellationToken ct = default); /// /// Get a list of open orders /// /// [Optional] The symbol to get open orders for, required for some exchanges, ignored otherwise + /// [Optional] Cancellation token for cancelling the request /// - Task>> GetOpenOrdersAsync(string? symbol = null); + Task>> GetOpenOrdersAsync(string? symbol = null, CancellationToken ct = default); /// /// Get a list of closed orders /// /// [Optional] The symbol to get closed orders for, required for some exchanges, ignored otherwise + /// [Optional] Cancellation token for cancelling the request /// - Task>> GetClosedOrdersAsync(string? symbol = null); + Task>> GetClosedOrdersAsync(string? symbol = null, CancellationToken ct = default); /// /// Cancel an order by id /// /// The id /// [Optional] The symbol the order is on, required for some exchanges, ignored otherwise + /// [Optional] Cancellation token for cancelling the request /// - Task> CancelOrderAsync(string orderId, string? symbol = null); + Task> CancelOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default); } /// @@ -138,14 +151,16 @@ namespace CryptoExchange.Net.Interfaces /// The price of the order, only for limit orders /// [Optional] The account id to place the order on, required for some exchanges, ignored otherwise /// [Optional] Leverage for this order. This is needed for some exchanges. For exchanges where this is not needed this parameter is ignored (and should be set before hand) + /// [Optional] Cancellation token for cancelling the request /// The id of the resulting order - Task> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, int? leverage = null, string? accountId = null); + Task> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, int? leverage = null, string? accountId = null, CancellationToken ct = default); /// /// Get position /// + /// [Optional] Cancellation token for cancelling the request /// - Task>> GetPositionsAsync(); + Task>> GetPositionsAsync(CancellationToken ct = default); } /// @@ -162,7 +177,8 @@ namespace CryptoExchange.Net.Interfaces /// The quantity of the order /// The price of the order, only for limit orders /// [Optional] The account id to place the order on, required for some exchanges, ignored otherwise + /// [Optional] Cancellation token for cancelling the request /// The id of the resulting order - Task> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null); + Task> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs index 9cf4f2d..ece5f1d 100644 --- a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Objects; @@ -88,8 +89,9 @@ namespace CryptoExchange.Net.Interfaces /// /// Start connecting and synchronizing the order book /// + /// A cancellation token to stop the order book when canceled /// - Task> StartAsync(); + Task> StartAsync(CancellationToken? ct = null); /// /// Stop syncing the order book diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 84df703..75a0de2 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -26,6 +26,7 @@ namespace CryptoExchange.Net.OrderBook private bool _stopProcessing; private Task? _processTask; + private CancellationTokenSource _cts; private readonly AsyncResetEvent _queueEvent; private readonly ConcurrentQueue _processQueue; @@ -220,22 +221,42 @@ namespace CryptoExchange.Net.OrderBook } /// - public async Task> StartAsync() + public async Task> 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().ConfigureAwait(false); + var startResult = await DoStartAsync(_cts.Token).ConfigureAwait(false); if (!startResult) { Status = OrderBookStatus.Disconnected; return new CallResult(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(new CancellationRequestedError()); + } + _subscription = startResult.Data; _subscription.ConnectionLost += () => { @@ -260,6 +281,7 @@ namespace CryptoExchange.Net.OrderBook { log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopping"); Status = OrderBookStatus.Disconnected; + _cts.Cancel(); _queueEvent.Set(); if (_processTask != null) await _processTask.ConfigureAwait(false); @@ -305,7 +327,7 @@ namespace CryptoExchange.Net.OrderBook /// and setting the initial order book /// /// - protected abstract Task> DoStartAsync(); + protected abstract Task> DoStartAsync(CancellationToken ct); /// /// Reset the order book @@ -316,7 +338,7 @@ namespace CryptoExchange.Net.OrderBook /// Resync the order book /// /// - protected abstract Task> DoResyncAsync(); + protected abstract Task> DoResyncAsync(CancellationToken ct); /// /// Implementation for validating a checksum value with the current order book. If checksum validation fails (returns false) @@ -459,16 +481,25 @@ namespace CryptoExchange.Net.OrderBook /// Wait until the order book snapshot has been set /// /// Max wait time + /// Cancellation token /// - protected async Task> WaitForSetOrderBookAsync(int timeout) + protected async Task> WaitForSetOrderBookAsync(int timeout, CancellationToken ct) { var startWait = DateTime.UtcNow; while (!bookSet && Status == OrderBookStatus.Syncing) { + if(ct.IsCancellationRequested) + return new CallResult(new CancellationRequestedError()); + if ((DateTime.UtcNow - startWait).TotalMilliseconds > timeout) return new CallResult(new ServerError("Timeout while waiting for data")); - await Task.Delay(10).ConfigureAwait(false); + try + { + await Task.Delay(10, ct).ConfigureAwait(false); + } + catch (OperationCanceledException) + { } } return new CallResult(true); @@ -533,7 +564,7 @@ namespace CryptoExchange.Net.OrderBook if (Status != OrderBookStatus.Syncing) return; - var resyncResult = await DoResyncAsync().ConfigureAwait(false); + var resyncResult = await DoResyncAsync(_cts.Token).ConfigureAwait(false); success = resyncResult; } @@ -665,6 +696,12 @@ namespace CryptoExchange.Net.OrderBook Status = OrderBookStatus.Syncing; _ = Task.Run(async () => { + if(_subscription == null) + { + Status = OrderBookStatus.Disconnected; + return; + } + await _subscription!.UnsubscribeAsync().ConfigureAwait(false); Reset(); _stopProcessing = false;