public void PrintSymbols(IExchangeAPI api) { api.GetSymbols().ToList().ForEach(s => Console.WriteLine(s)); }
/// <summary> /// Constructor /// </summary> /// <param name="api">Exchange API</param> /// <param name="symbol">The symbol to trade by default, can be null</param> public ExchangeInfo(IExchangeAPI api, string symbol = null) { API = api; Symbols = api.GetSymbols().ToArray(); TradeInfo = new ExchangeTradeInfo(this, symbol); }
public ActionResult BacktesterResults(string exchange, string coinsToBuy, string baseCurrency, bool saveSignals, decimal startingWallet, decimal tradeAmount, DateTime?from = null, DateTime?to = null, string candleSize = "5", string strategy = "all") { var strategies = new JObject(); var coins = new List <string>(); if (String.IsNullOrEmpty(coinsToBuy)) { IExchangeAPI api = ExchangeAPI.GetExchangeAPI(exchange); var exchangeCoins = api.GetSymbolsMetadataAsync().Result.Where(m => m.BaseCurrency == baseCurrency); foreach (var coin in exchangeCoins) { coins.Add(api.ExchangeSymbolToGlobalSymbol(coin.MarketName)); } } else { Char delimiter = ','; String[] coinsToBuyArray = coinsToBuy.Split(delimiter); foreach (var coin in coinsToBuyArray) { coins.Add(coin.ToUpper()); } } var backtestOptions = new BacktestOptions { DataFolder = Global.DataPath, Exchange = (Exchange)Enum.Parse(typeof(Exchange), exchange, true), Coins = coins, CandlePeriod = Int32.Parse(candleSize) }; if (from.HasValue) { backtestOptions.StartDate = from.Value; } if (to.HasValue) { backtestOptions.EndDate = to.Value; backtestOptions.EndDate = backtestOptions.EndDate.AddDays(1).AddMinutes(-1); } if (tradeAmount == 0m) { tradeAmount = backtestOptions.StakeAmount; } if (startingWallet == 0m) { startingWallet = backtestOptions.StartingWallet; } var cts = new CancellationTokenSource(); var parallelOptions = new ParallelOptions { CancellationToken = cts.Token, MaxDegreeOfParallelism = Environment.ProcessorCount }; Parallel.ForEach(BacktestFunctions.GetTradingStrategies(), parallelOptions, async tradingStrategy => { if (strategy != "all") { var base64EncodedBytes = Convert.FromBase64String(strategy); if (tradingStrategy.Name != Encoding.UTF8.GetString(base64EncodedBytes)) { return; } } var result = await BacktestFunctions.BackTestJson(tradingStrategy, backtestOptions, Global.DataStoreBacktest, baseCurrency, saveSignals, startingWallet, tradeAmount); for (int i = 0; i < result.Count(); i++) { if (i == 0) { await Runtime.GlobalHubBacktest.Clients.All.SendAsync("SendSummary", JsonConvert.SerializeObject(result[i])); } else { await Runtime.GlobalHubBacktest.Clients.All.SendAsync("Send", JsonConvert.SerializeObject(result[i])); } } }); return(new JsonResult(strategies)); }
/// <summary> /// Constructor /// </summary> /// <param name="api">Exchange API</param> /// <param name="marketSymbol">The market symbol to trade by default, can be null</param> public ExchangeInfo(IExchangeAPI api, string marketSymbol = null) { API = api; MarketSymbols = api.GetMarketSymbolsAsync().Sync().ToArray(); TradeInfo = new ExchangeTradeInfo(this, marketSymbol); }
/// <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).</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">Ticker symbols or null/empty for all of them (if supported)</param> /// <returns>Web socket, call Dispose to close</returns> public static IWebSocket GetOrderBookWebSocket(this IExchangeAPI api, Action <ExchangeOrderBook> callback, int maxCount = 20, params string[] symbols) { // 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. var fullBooks = new ConcurrentDictionary <string, ExchangeOrderBook>(); var freshBooksQueue = new Dictionary <string, Queue <ExchangeOrderBook> >(); var fullBookRequestLock = new HashSet <string>(); 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 freshBook) { bool foundFullBook = fullBooks.TryGetValue(freshBook.Symbol, out ExchangeOrderBook fullOrderBook); switch (api.Name) { // 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 case ExchangeName.Bittrex: case ExchangeName.Binance: case ExchangeName.Poloniex: { if (!foundFullBook) { // attempt to find the right queue to put the partial order book in to be processed later lock (freshBooksQueue) { if (!freshBooksQueue.TryGetValue(freshBook.Symbol, out Queue <ExchangeOrderBook> freshQueue)) { // no queue found, make a new one freshBooksQueue[freshBook.Symbol] = freshQueue = new Queue <ExchangeOrderBook>(); } freshQueue.Enqueue(freshBook); } bool makeRequest; lock (fullBookRequestLock) { makeRequest = fullBookRequestLock.Add(freshBook.Symbol); } if (makeRequest) { // we are the first to see this symbol, make a full request to API fullBooks[freshBook.Symbol] = fullOrderBook = await api.GetOrderBookAsync(freshBook.Symbol, maxCount); fullOrderBook.Symbol = freshBook.Symbol; // now that we have the full order book, we can process it (and any books in the queue) } else { // stop processing, other code will take these items out of the queue later return; } } // check if any old books for this symbol, if so process them first lock (freshBooksQueue) { if (freshBooksQueue.TryGetValue(freshBook.Symbol, out Queue <ExchangeOrderBook> freshQueue)) { while (freshQueue.Count != 0) { updateOrderBook(fullOrderBook, freshQueue.Dequeue()); } } } break; } // First response from exchange will be the full order book. // Subsequent updates will be deltas, at least some exchanges have their heads on straight case ExchangeName.BitMEX: case ExchangeName.Okex: case ExchangeName.Coinbase: { if (!foundFullBook) { fullBooks[freshBook.Symbol] = fullOrderBook = freshBook; } else { updateOrderBook(fullOrderBook, freshBook); } break; } // Websocket always returns full order book case ExchangeName.Huobi: { fullBooks[freshBook.Symbol] = fullOrderBook = freshBook; break; } default: throw new NotSupportedException("Full order book web socket not supported for exchange " + api.Name); } fullOrderBook.LastUpdatedUtc = DateTime.UtcNow; callback(fullOrderBook); } IWebSocket socket = api.GetOrderBookDeltasWebSocket(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 (freshBooksQueue) { freshBooksQueue.Clear(); } lock (fullBookRequestLock) { fullBookRequestLock.Clear(); } return(Task.CompletedTask); }; return(socket); }
public async Task <Tuple <DateTime, DateTime> > CacheAllData(ExchangeAPI api, Exchange exchange) { Global.Logger.Information($"Starting CacheAllData"); var watch1 = System.Diagnostics.Stopwatch.StartNew(); var exchangeCoins = api.GetMarketSymbolsMetadataAsync().Result.Where(m => m.BaseCurrency == Global.Configuration.TradeOptions.QuoteCurrency); // If there are items on the only trade list remove the rest if (Global.Configuration.TradeOptions.OnlyTradeList.Count > 0) { exchangeCoins = exchangeCoins.Where(m => Global.Configuration.TradeOptions.OnlyTradeList.Any(c => c.Contains(m.MarketSymbol))).ToList(); } var currentExchangeOption = Global.Configuration.ExchangeOptions.FirstOrDefault(); IExchangeAPI realExchange = ExchangeAPI.GetExchangeAPI(api.Name); var returns = new Tuple <DateTime, DateTime>(DateTime.MinValue, DateTime.MinValue); foreach (var coin in exchangeCoins) { var symbol = coin.MarketSymbol; if (realExchange is ExchangeBinanceAPI) { symbol = await api.ExchangeMarketSymbolToGlobalMarketSymbolAsync(symbol); } var backtestOptions = new BacktestOptions { DataFolder = Global.DataPath, Exchange = exchange, Coin = symbol, CandlePeriod = Int32.Parse(currentExchangeOption.SimulationCandleSize) }; var key1 = api.Name + backtestOptions.Coin + backtestOptions.CandlePeriod; var data1 = Global.AppCache.Get <List <Candle> >(key1); if (data1 != null) { returns = new Tuple <DateTime, DateTime>(data1.First().Timestamp, data1.Last().Timestamp); continue; } Candle databaseFirstCandle = Global.DataStoreBacktest.GetBacktestFirstCandle(backtestOptions).Result; Candle databaseLastCandle = Global.DataStoreBacktest.GetBacktestLastCandle(backtestOptions).Result; if (databaseFirstCandle == null || databaseLastCandle == null) { continue; } backtestOptions.StartDate = databaseFirstCandle.Timestamp; backtestOptions.EndDate = databaseLastCandle.Timestamp; var candleProvider = new DatabaseCandleProvider(); var _candle15 = candleProvider.GetCandles(backtestOptions, Global.DataStoreBacktest).Result; _candle15 = await _candle15.FillCandleGaps((Period)Enum.Parse(typeof(Period), backtestOptions.CandlePeriod.ToString(), true)); Global.AppCache.Remove(backtestOptions.Coin + backtestOptions.CandlePeriod); Global.AppCache.Add(api.Name + backtestOptions.Coin + backtestOptions.CandlePeriod, _candle15, new MemoryCacheEntryOptions()); Global.Logger.Information($" Cached {key1}"); backtestOptions.CandlePeriod = 1; var key2 = api.Name + backtestOptions.Coin + backtestOptions.CandlePeriod; if (Global.AppCache.Get <List <Candle> >(key2) != null) { continue; } Candle database1FirstCandle = Global.DataStoreBacktest.GetBacktestFirstCandle(backtestOptions).Result; Candle database1LastCandle = Global.DataStoreBacktest.GetBacktestLastCandle(backtestOptions).Result; if (database1FirstCandle == null || database1LastCandle == null) { continue; } backtestOptions.StartDate = database1FirstCandle.Timestamp; backtestOptions.EndDate = database1LastCandle.Timestamp; var _candle1 = candleProvider.GetCandles(backtestOptions, Global.DataStoreBacktest).Result; _candle1 = await _candle1.FillCandleGaps((Period)Enum.Parse(typeof(Period), backtestOptions.CandlePeriod.ToString(), true)); Global.AppCache.Remove(backtestOptions.Coin + backtestOptions.CandlePeriod); Global.AppCache.Add(api.Name + backtestOptions.Coin + backtestOptions.CandlePeriod, _candle1, new MemoryCacheEntryOptions()); Global.Logger.Information($" Cached {key2}"); returns = new Tuple <DateTime, DateTime>(backtestOptions.StartDate, backtestOptions.EndDate); } watch1.Stop(); Global.Logger.Warning($"Ended CacheAllData in #{watch1.Elapsed.TotalSeconds} seconds"); return(returns); }
public Data GetGata(string market, IExchangeAPI exchange) { return(new Data { DateTime = DateTime.Now, Price = GetFirstAsk(market, exchange) }); }
public void Save() { stExchange = API; }
public override async Task RunCommand() { var marketSymbol = "BTC-USD"; var marketSymbol2 = "XXBTZUSD"; IExchangeAPI apiCoinbase = await ExchangeAPI.GetExchangeAPIAsync(ExchangeName.Coinbase), apiGemini = await ExchangeAPI.GetExchangeAPIAsync(ExchangeName.Gemini), apiKraken = await ExchangeAPI.GetExchangeAPIAsync(ExchangeName.Kraken), apiBitfinex = await ExchangeAPI.GetExchangeAPIAsync(ExchangeName.Bitfinex); //TODO: Make this multi-threaded and add parameters Console.WriteLine("Use CTRL-C to stop."); while (true) { var ticker = await apiCoinbase.GetTickerAsync(marketSymbol); var orders = await apiCoinbase.GetOrderBookAsync(marketSymbol); var askAmountSum = orders.Asks.Values.Sum(o => o.Amount); var askPriceSum = orders.Asks.Values.Sum(o => o.Price); var bidAmountSum = orders.Bids.Values.Sum(o => o.Amount); var bidPriceSum = orders.Bids.Values.Sum(o => o.Price); var ticker2 = await apiGemini.GetTickerAsync(marketSymbol); var orders2 = await apiGemini.GetOrderBookAsync(marketSymbol); var askAmountSum2 = orders2.Asks.Values.Sum(o => o.Amount); var askPriceSum2 = orders2.Asks.Values.Sum(o => o.Price); var bidAmountSum2 = orders2.Bids.Values.Sum(o => o.Amount); var bidPriceSum2 = orders2.Bids.Values.Sum(o => o.Price); var ticker3 = await apiKraken.GetTickerAsync(marketSymbol2); var orders3 = await apiKraken.GetOrderBookAsync(marketSymbol2); var askAmountSum3 = orders3.Asks.Values.Sum(o => o.Amount); var askPriceSum3 = orders3.Asks.Values.Sum(o => o.Price); var bidAmountSum3 = orders3.Bids.Values.Sum(o => o.Amount); var bidPriceSum3 = orders3.Bids.Values.Sum(o => o.Price); var ticker4 = await apiBitfinex.GetTickerAsync(marketSymbol); var orders4 = await apiBitfinex.GetOrderBookAsync(marketSymbol); var askAmountSum4 = orders4.Asks.Values.Sum(o => o.Amount); var askPriceSum4 = orders4.Asks.Values.Sum(o => o.Price); var bidAmountSum4 = orders4.Bids.Values.Sum(o => o.Amount); var bidPriceSum4 = orders4.Bids.Values.Sum(o => o.Price); Console.Clear(); Console.WriteLine("GDAX: {0,13:N}, {1,15:N}, {2,8:N}, {3,13:N}, {4,8:N}, {5,13:N}", ticker.Last, ticker.Volume.QuoteCurrencyVolume, askAmountSum, askPriceSum, bidAmountSum, bidPriceSum); Console.WriteLine("GEMI: {0,13:N}, {1,15:N}, {2,8:N}, {3,13:N}, {4,8:N}, {5,13:N}", ticker2.Last, ticker2.Volume.QuoteCurrencyVolume, askAmountSum2, askPriceSum2, bidAmountSum2, bidPriceSum2); Console.WriteLine("KRAK: {0,13:N}, {1,15:N}, {2,8:N}, {3,13:N}, {4,8:N}, {5,13:N}", ticker3.Last, ticker3.Volume.QuoteCurrencyVolume, askAmountSum3, askPriceSum3, bidAmountSum3, bidPriceSum3); Console.WriteLine("BITF: {0,13:N}, {1,15:N}, {2,8:N}, {3,13:N}, {4,8:N}, {5,13:N}", ticker4.Last, ticker4.Volume.QuoteCurrencyVolume, askAmountSum4, askPriceSum4, bidAmountSum4, bidPriceSum4); Thread.Sleep(IntervalMs); } }
/// <summary> /// Get full order book bids and asks via web socket. This is efficient and will only use the order book deltas. /// </summary> /// <param name="callback">Callback of symbol, order book</param> /// <param name="maxCount">Max count of bids and asks - not all exchanges will honor this parameter</param> /// <param name="symbol">Ticker symbols or null/empty for all of them (if supported)</param> /// <returns>Web socket, call Dispose to close</returns> public static IDisposable GetOrderBookWebSocket(this IExchangeAPI api, Action <ExchangeOrderBook> callback, int maxCount = 20, params string[] symbols) { // Gets a delta socket for a collection of order books, then maintains a full order book. // The suggested way to use this is: // 1. Open this socket and begin buffering events you receive // 2. Get a depth snapshot of the order books you care about // 3. Drop any event where SequenceNumber is less than or equal to the snapshot last update id // Notes: // * Confirm with the Exchange's API docs whether the data in each event is the absolute quantity or differential quantity // * If the quantity is 0, remove the price level // * Receiving an event that removes a price level that is not in your local order book can happen and is normal. // Dictionary <string, ExchangeOrderBook> fullBooks = new Dictionary <string, ExchangeOrderBook>(); void innerCallback(ExchangeOrderBook book) { if (api.Name == ExchangeName.Binance) { // see if we have a full order book for the symbol if (!fullBooks.TryGetValue(book.Symbol, out ExchangeOrderBook fullBook)) { fullBooks[book.Symbol] = fullBook = api.GetOrderBook(book.Symbol, 1000); fullBook.Symbol = book.Symbol; } // update deltas as long as the full book is at or before the delta timestamp if (fullBook.SequenceId <= book.SequenceId) { foreach (var ask in book.Asks) { if (ask.Value.Amount <= 0m || ask.Value.Price <= 0m) { fullBook.Asks.Remove(ask.Value.Price); } else { fullBook.Asks[ask.Value.Price] = ask.Value; } } foreach (var bid in book.Bids) { if (bid.Value.Amount <= 0m || bid.Value.Price <= 0m) { fullBook.Bids.Remove(bid.Value.Price); } else { fullBook.Bids[bid.Value.Price] = bid.Value; } } fullBook.SequenceId = book.SequenceId; } callback(fullBook); } else if (api.Name == ExchangeName.Okex || api.Name == ExchangeName.BitMEX) { if (!fullBooks.TryGetValue(book.Symbol, out ExchangeOrderBook fullBook)) { fullBooks[book.Symbol] = book; fullBook.Symbol = book.Symbol; } foreach (var ask in book.Asks) { if (ask.Value.Amount <= 0m || ask.Value.Price <= 0m) { fullBook.Asks.Remove(ask.Value.Price); } else { fullBook.Asks[ask.Value.Price] = ask.Value; } } foreach (var bid in book.Bids) { if (bid.Value.Amount <= 0m || bid.Value.Price <= 0m) { fullBook.Bids.Remove(bid.Value.Price); } else { fullBook.Bids[bid.Value.Price] = bid.Value; } } callback(fullBook); } else if (api.Name == ExchangeName.Huobi) { fullBooks[book.Symbol] = book; } }; return(api.GetOrderBookDeltasWebSocket(innerCallback, maxCount, symbols)); }
/// <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).</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">Ticker symbols or null/empty for all of them (if supported)</param> /// <returns>Web socket, call Dispose to close</returns> public static IDisposable GetOrderBookWebSocket(this IExchangeAPI api, Action <ExchangeOrderBook> callback, int maxCount = 20, params string[] symbols) { // 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. var fullBooks = new Dictionary <string, ExchangeOrderBook>(); void innerCallback(ExchangeOrderBook freshBook) { bool foundFullBook = fullBooks.TryGetValue(freshBook.Symbol, out ExchangeOrderBook fullOrderBook); switch (api.Name) { // Fetch an initial book the first time and apply deltas on top case ExchangeName.Bittrex: case ExchangeName.Binance: case ExchangeName.Poloniex: { // If we don't have an initial order book for this symbol, fetch it if (!foundFullBook) { fullBooks[freshBook.Symbol] = fullOrderBook = api.GetOrderBook(freshBook.Symbol, 1000); fullOrderBook.Symbol = freshBook.Symbol; } // 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; } break; } // First response from exchange will be the full order book. // Subsequent updates will be deltas case ExchangeName.BitMEX: case ExchangeName.Okex: { if (!foundFullBook) { fullBooks[freshBook.Symbol] = fullOrderBook = freshBook; fullOrderBook.Symbol = freshBook.Symbol; } else { ApplyDelta(freshBook.Asks, fullOrderBook.Asks); ApplyDelta(freshBook.Bids, fullOrderBook.Bids); } break; } // Websocket always returns full order book case ExchangeName.Huobi: fullBooks[freshBook.Symbol] = fullOrderBook = freshBook; break; } callback(fullOrderBook); } return(api.GetOrderBookDeltasWebSocket(innerCallback, maxCount, symbols)); }
// Helper method for SELL orders (ASYNC) public static async Task <ExchangeOrderResult> SellAsync(this IExchangeAPI api, string symbol, decimal price, decimal amount) { return(await ApiHelper.PlaceOrderAsync(api, symbol, OrderSide.Sell, price, amount)); }
// Helper method for SELL orders public static ExchangeOrderResult Sell(this IExchangeAPI api, string symbol, decimal price, decimal amount) { return(ApiHelper.PlaceOrder(api, symbol, OrderSide.Sell, price, amount)); }
/// <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).</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">Ticker symbols or null/empty for all of them (if supported)</param> /// <returns>Web socket, call Dispose to close</returns> public static IWebSocket GetOrderBookWebSocket(this IExchangeAPI api, Action <ExchangeOrderBook> callback, int maxCount = 20, params string[] symbols) { // 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. var fullBooks = new ConcurrentDictionary <string, ExchangeOrderBook>(); var 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.Symbol, out ExchangeOrderBook fullOrderBook); switch (api.Name) { // 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 case ExchangeName.Bittrex: case ExchangeName.Binance: case ExchangeName.Poloniex: { 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.Symbol, out partialOrderBookQueue)) { // no queue found, make a new one partialOrderBookQueues[newOrderBook.Symbol] = 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.Symbol, maxCount); fullOrderBook.Symbol = newOrderBook.Symbol; fullBooks[newOrderBook.Symbol] = 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.Symbol, out partialOrderBookQueue); } if (partialOrderBookQueue != null) { // lock the individual queue for processing, fifo queue lock (partialOrderBookQueue) { while (partialOrderBookQueue.Count != 0) { updateOrderBook(fullOrderBook, partialOrderBookQueue.Dequeue()); } } } break; } // First response from exchange will be the full order book. // Subsequent updates will be deltas, at least some exchanges have their heads on straight case ExchangeName.BitMEX: case ExchangeName.Okex: case ExchangeName.Coinbase: { if (!foundFullBook) { fullBooks[newOrderBook.Symbol] = fullOrderBook = newOrderBook; } else { updateOrderBook(fullOrderBook, newOrderBook); } break; } // Websocket always returns full order book, WTF...? case ExchangeName.Huobi: { fullBooks[newOrderBook.Symbol] = fullOrderBook = newOrderBook; break; } default: throw new NotSupportedException("Full order book web socket not supported for exchange " + api.Name); } fullOrderBook.LastUpdatedUtc = DateTime.UtcNow; callback(fullOrderBook); } IWebSocket socket = api.GetOrderBookDeltasWebSocket(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); }
/// <summary> /// Constructor /// </summary> /// <param name="api">Exchange API</param> /// <param name="marketSymbols">Market symbols</param> /// <param name="marketSymbol">The market symbol to trade by default, can be null</param> public ExchangeInfo(IExchangeAPI api, IReadOnlyCollection <string> marketSymbols, string marketSymbol = null) { API = api; MarketSymbols = marketSymbols; TradeInfo = new ExchangeTradeInfo(this, marketSymbol); }
/// <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).</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">Ticker symbols or null/empty for all of them (if supported)</param> /// <returns>Web socket, call Dispose to close</returns> public static IWebSocket GetOrderBookWebSocket(this IExchangeAPI api, Action <ExchangeOrderBook> callback, int maxCount = 20, params string[] symbols) { // 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. var fullBooks = new ConcurrentDictionary <string, 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 freshBook) { bool foundFullBook = fullBooks.TryGetValue(freshBook.Symbol, out ExchangeOrderBook fullOrderBook); switch (api.Name) { // Fetch an initial book the first time and apply deltas on top case ExchangeName.Bittrex: case ExchangeName.Binance: case ExchangeName.Poloniex: { // If we don't have an initial order book for this symbol, fetch it if (!foundFullBook) { fullBooks[freshBook.Symbol] = fullOrderBook = await api.GetOrderBookAsync(freshBook.Symbol, 1000); fullOrderBook.Symbol = freshBook.Symbol; } else { updateOrderBook(fullOrderBook, freshBook); } break; } // First response from exchange will be the full order book. // Subsequent updates will be deltas case ExchangeName.Okex: { if (!foundFullBook) { fullBooks[freshBook.Symbol] = fullOrderBook = freshBook; } else { updateOrderBook(fullOrderBook, freshBook); } break; } // Websocket always returns full order book case ExchangeName.Huobi: { fullBooks[freshBook.Symbol] = fullOrderBook = freshBook; break; } } fullOrderBook.LastUpdatedUtc = DateTime.UtcNow; callback(fullOrderBook); } IWebSocket socket = api.GetOrderBookDeltasWebSocket(async(b) => await innerCallback(b), 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(); }; return(socket); }
public NewExchangeAPI(IExchangeAPI exchangeAPI) { this.API = exchangeAPI; }