diff --git a/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs b/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs index f596e58..ce098f0 100644 --- a/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs +++ b/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs @@ -108,5 +108,33 @@ namespace CryptoExchange.Net.UnitTests Assert.AreEqual(1.06666667m, resultBids2.Data); Assert.AreEqual(1.23333333m, resultAsks2.Data); } + + [TestCase] + public void CalculateTradableAmount() + { + var orderbook = new TestableSymbolOrderBook(); + orderbook.SetData(new List + { + new BookEntry{ Price = 1, Quantity = 1 }, + new BookEntry{ Price = 1.1m, Quantity = 1 }, + }, + new List() + { + new BookEntry{ Price = 1.2m, Quantity = 1 }, + new BookEntry{ Price = 1.3m, Quantity = 1 }, + }); + + var resultBids = orderbook.CalculateTradableAmount(2, OrderBookEntryType.Bid); + var resultAsks = orderbook.CalculateTradableAmount(2, OrderBookEntryType.Ask); + var resultBids2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Bid); + var resultAsks2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Ask); + + Assert.True(resultBids.Success); + Assert.True(resultAsks.Success); + Assert.AreEqual(1.9m, resultBids.Data); + Assert.AreEqual(1.61538462m, resultAsks.Data); + Assert.AreEqual(1.4m, resultBids2.Data); + Assert.AreEqual(1.23076923m, resultAsks2.Data); + } } } diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs index ece5f1d..ec39d27 100644 --- a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs @@ -101,13 +101,22 @@ namespace CryptoExchange.Net.Interfaces /// /// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled - /// at that price since between this calculation and the order placement the book can have changed. + /// at that price since between this calculation and the order placement the book might have changed. /// /// The quantity in base asset to fill /// The type /// Average fill price CallResult CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type); + /// + /// Get the amount of base asset which can be traded with the quote quantity when placing a market order at at the current order book state. + /// This is no guarentee that an order of that quantity would actually be fill the quantity returned by this since between this calculation and the order placement the book might have changed. + /// + /// The quantity in quote asset looking to trade + /// The type + /// Amount of base asset tradable with the specified amount of quote asset + CallResult CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type); + /// /// String representation of the top x entries /// diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index d6be9e7..f87457b 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -304,14 +304,14 @@ namespace CryptoExchange.Net.OrderBook } /// - public CallResult CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type) + public CallResult CalculateAverageFillPrice(decimal baseQuantity, OrderBookEntryType type) { if (Status != OrderBookStatus.Synced) return new CallResult(new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state")); var totalCost = 0m; var totalAmount = 0m; - var amountLeft = quantity; + var amountLeft = baseQuantity; lock (_bookLock) { var list = type == OrderBookEntryType.Ask ? asks : bids; @@ -334,6 +334,35 @@ namespace CryptoExchange.Net.OrderBook return new CallResult(Math.Round(totalCost / totalAmount, 8)); } + /// + public CallResult CalculateTradableAmount(decimal quoteQuantity, OrderBookEntryType type) + { + if (Status != OrderBookStatus.Synced) + return new CallResult(new InvalidOperationError($"{nameof(CalculateTradableAmount)} is not available when book is not in Synced state")); + + var quoteQuantityLeft = quoteQuantity; + var totalBaseQuantity = 0m; + lock (_bookLock) + { + var list = type == OrderBookEntryType.Ask ? asks : bids; + + var step = 0; + while (quoteQuantityLeft > 0) + { + if (step == list.Count) + return new CallResult(new InvalidOperationError("Quantity is larger than order in the order book")); + + var element = list.ElementAt(step); + var stepAmount = Math.Min(element.Value.Quantity * element.Value.Price, quoteQuantityLeft); + quoteQuantityLeft -= stepAmount; + totalBaseQuantity += stepAmount / element.Value.Price; + step++; + } + } + + return new CallResult(Math.Round(totalBaseQuantity, 8)); + } + /// /// Implementation for starting the order book. Should typically have logic for subscribing to the update stream and retrieving /// and setting the initial order book