private void GetOrderBook(IOrderBookProvider provider, OrderBookContext context) { if (context == null) { return; } try { var r = AsyncContext.Run(() => provider.GetOrderBookAsync(context)); Assert.IsTrue(r != null); if (context.MaxRecordsCount.HasValue) { Assert.IsTrue(r.Count == context.MaxRecordsCount.Value); } else { Assert.IsTrue(r.Count > 0); } Trace.WriteLine($"Order book data ({r.Count(x => x.Type == OrderType.Ask)} asks, {r.Count(x => x.Type == OrderType.Bid)} bids): "); foreach (var obr in r) { Trace.WriteLine($"{obr.UtcUpdated} | For {context.Pair.Asset1}: {obr.Type} {obr.Price.Display}, {obr.Volume} "); } } catch (Exception e) { Assert.Fail(e.Message); } }
private void InternalGetOrderBook(IOrderBookProvider provider, OrderBookContext context, bool priceLessThan1) { var r = AsyncContext.Run(() => provider.GetOrderBookAsync(context)); Assert.IsTrue(r != null, "Null response returned"); if (r.IsReversed) { Trace.WriteLine("Asset pair is reversed"); } // Assert.IsTrue(r.Pair.Equals(context.Pair), "Incorrect asset pair returned"); Assert.IsTrue(r.Asks.Count > 0, "No asks returned"); Assert.IsTrue(r.Bids.Count > 0, "No bids returned"); Assert.IsTrue(r.Asks.Count <= context.MaxRecordsCount, "Incorrect number of ask order book records returned"); Assert.IsTrue(r.Bids.Count <= context.MaxRecordsCount, "Incorrect number of bid order book records returned"); //if (context.MaxRecordsCount == Int32.MaxValue) // Assert.IsTrue(r.Count > 0, "No order book records returned"); //else // Assert.IsTrue(r.Asks.Count == context.MaxRecordsCount && r.Bids.Count == context.MaxRecordsCount, "Incorrect number of order book records returned"); Trace.WriteLine($"Highest bid: {r.HighestBid}"); Trace.WriteLine($"Lowest ask: {r.LowestAsk}"); var records = new List <OrderBookRecord>() { r.LowestAsk, r.HighestBid }; foreach (var record in records) { if (priceLessThan1) // Checks if the pair is reversed (price-wise). { Assert.IsTrue(record.Price < 1, "Reverse check failed. Price is expected to be < 1"); } else { Assert.IsTrue(record.Price > 1, "Reverse check failed. Price is expected to be > 1"); } } Trace.WriteLine($"Order book data ({r.Asks.Count} asks, {r.Bids.Count} bids): "); foreach (var obr in r.Asks.Concat(r.Bids)) { Trace.WriteLine($"{obr.UtcUpdated} : {obr}"); } }
private void InternalGetOrderBook(IOrderBookProvider provider, OrderBookContext context, bool priceLessThan1) { var r = AsyncContext.Run(() => provider.GetOrderBookAsync(context)); Assert.IsTrue(r != null, "Null response returned"); if (r.Pair.Reversed.Equals(context.Pair)) { Trace.WriteLine("Asset pair is reversed"); } // Assert.IsTrue(r.Pair.Equals(context.Pair), "Incorrect asset pair returned"); if (context.MaxRecordsCount == Int32.MaxValue) { Assert.IsTrue(r.Count > 0, "No order book records returned"); } else { Assert.IsTrue(r.Asks.Count == context.MaxRecordsCount && r.Bids.Count == context.MaxRecordsCount, "Incorrect number of order book records returned"); } foreach (var record in r.Asks.Take(1).Concat(r.Bids.Take(1))) { if (priceLessThan1) // Checks if the pair is reversed (price-wise). { Assert.IsTrue(record.Price < 1, "Reverse check failed. Price is expected to be < 1"); } else { Assert.IsTrue(record.Price > 1, "Reverse check failed. Price is expected to be > 1"); } } Trace.WriteLine($"Order book data ({r.Asks.Count} asks, {r.Bids.Count} bids): "); foreach (var obr in r.Asks.Concat(r.Bids)) { Trace.WriteLine($"{obr.UtcUpdated} | For {context.Pair.Asset1}: {obr.Type} {obr.Price.Display}, {obr.Volume} "); } }
/// <summary> /// Get full order book bids and asks via web socket. This is efficient and will /// only use the order book deltas (if supported by the exchange). This method deals /// with the complexity of different exchanges sending order books that are full, /// partial or otherwise. /// </summary> /// <param name="callback">Callback containing full order book</param> /// <param name="maxCount">Max count of bids and asks - not all exchanges will honor this /// parameter</param> /// <param name="symbols">Order book symbols or null/empty for all of them (if supported)</param> /// <returns>Web socket, call Dispose to close</returns> public static IWebSocket GetFullOrderBookWebSocket(this IOrderBookProvider api, Action <ExchangeOrderBook> callback, int maxCount = 20, params string[] symbols) { if (api.WebSocketOrderBookType == WebSocketOrderBookType.None) { throw new NotSupportedException(api.GetType().Name + " does not support web socket order books"); } // Notes: // * Confirm with the Exchange's API docs whether the data in each event is the absolute quantity or differential quantity // * Receiving an event that removes a price level that is not in your local order book can happen and is normal. ConcurrentDictionary <string, ExchangeOrderBook> fullBooks = new ConcurrentDictionary <string, ExchangeOrderBook>(); Dictionary <string, Queue <ExchangeOrderBook> > partialOrderBookQueues = new Dictionary <string, Queue <ExchangeOrderBook> >(); void applyDelta(SortedDictionary <decimal, ExchangeOrderPrice> deltaValues, SortedDictionary <decimal, ExchangeOrderPrice> bookToEdit) { foreach (ExchangeOrderPrice record in deltaValues.Values) { if (record.Amount <= 0 || record.Price <= 0) { bookToEdit.Remove(record.Price); } else { bookToEdit[record.Price] = record; } } } void updateOrderBook(ExchangeOrderBook fullOrderBook, ExchangeOrderBook freshBook) { lock (fullOrderBook) { // update deltas as long as the full book is at or before the delta timestamp if (fullOrderBook.SequenceId <= freshBook.SequenceId) { applyDelta(freshBook.Asks, fullOrderBook.Asks); applyDelta(freshBook.Bids, fullOrderBook.Bids); fullOrderBook.SequenceId = freshBook.SequenceId; } } } async Task innerCallback(ExchangeOrderBook newOrderBook) { // depending on the exchange, newOrderBook may be a complete or partial order book // ideally all exchanges would send the full order book on first message, followed by delta order books // but this is not the case bool foundFullBook = fullBooks.TryGetValue(newOrderBook.MarketSymbol, out ExchangeOrderBook fullOrderBook); switch (api.WebSocketOrderBookType) { case WebSocketOrderBookType.DeltasOnly: { // Fetch an initial book the first time and apply deltas on top // send these exchanges scathing support tickets that they should send // the full book for the first web socket callback message Queue <ExchangeOrderBook> partialOrderBookQueue; bool requestFullOrderBook = false; // attempt to find the right queue to put the partial order book in to be processed later lock (partialOrderBookQueues) { if (!partialOrderBookQueues.TryGetValue(newOrderBook.MarketSymbol, out partialOrderBookQueue)) { // no queue found, make a new one partialOrderBookQueues[newOrderBook.MarketSymbol] = partialOrderBookQueue = new Queue <ExchangeOrderBook>(); requestFullOrderBook = !foundFullBook; } // always enqueue the partial order book, they get dequeued down below partialOrderBookQueue.Enqueue(newOrderBook); } // request the entire order book if we need it if (requestFullOrderBook) { fullOrderBook = await api.GetOrderBookAsync(newOrderBook.MarketSymbol, maxCount); fullOrderBook.MarketSymbol = newOrderBook.MarketSymbol; fullBooks[newOrderBook.MarketSymbol] = fullOrderBook; } else if (!foundFullBook) { // we got a partial book while the full order book was being requested // return out, the full order book loop will process this item in the queue return; } // else new partial book with full order book available, will get dequeued below // check if any old books for this symbol, if so process them first // lock dictionary of queues for lookup only lock (partialOrderBookQueues) { partialOrderBookQueues.TryGetValue(newOrderBook.MarketSymbol, out partialOrderBookQueue); } if (partialOrderBookQueue != null) { // lock the individual queue for processing, fifo queue lock (partialOrderBookQueue) { while (partialOrderBookQueue.Count != 0) { updateOrderBook(fullOrderBook, partialOrderBookQueue.Dequeue()); } } } } break; case WebSocketOrderBookType.FullBookFirstThenDeltas: { // First response from exchange will be the full order book. // Subsequent updates will be deltas, at least some exchanges have their heads on straight if (!foundFullBook) { fullBooks[newOrderBook.MarketSymbol] = fullOrderBook = newOrderBook; } else { updateOrderBook(fullOrderBook, newOrderBook); } } break; case WebSocketOrderBookType.FullBookAlways: { // Websocket always returns full order book, WTF...? fullBooks[newOrderBook.MarketSymbol] = fullOrderBook = newOrderBook; } break; } fullOrderBook.LastUpdatedUtc = CryptoUtility.UtcNow; callback(fullOrderBook); } IWebSocket socket = api.GetOrderBookWebSocket(async(b) => { try { await innerCallback(b); } catch { } }, maxCount, symbols); socket.Connected += (s) => { // when we re-connect, we must invalidate the order books, who knows how long we were disconnected // and how out of date the order books are fullBooks.Clear(); lock (partialOrderBookQueues) { partialOrderBookQueues.Clear(); } return(Task.CompletedTask); }; return(socket); }
public static Task <ApiResponse <OrderBook> > GetOrderBookAsync(IOrderBookProvider provider, OrderBookContext context) { return(ApiHelpers.WrapExceptionAsync(() => provider.GetOrderBookAsync(context), nameof(GetOrderBook), provider, context)); }