diff --git a/Brokerages/Bitfinex/BestBidAskUpdatedEventArgs.cs b/Brokerages/Bitfinex/BestBidAskUpdatedEventArgs.cs new file mode 100644 index 000000000000..753a2f976f31 --- /dev/null +++ b/Brokerages/Bitfinex/BestBidAskUpdatedEventArgs.cs @@ -0,0 +1,67 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; + +namespace QuantConnect.Brokerages.Bitfinex +{ + /// + /// Event arguments class for the event + /// + public sealed class BestBidAskUpdatedEventArgs : EventArgs + { + /// + /// Gets the new best bid price + /// + public Symbol Symbol { get; } + + /// + /// Gets the new best bid price + /// + public decimal BestBidPrice { get; } + + /// + /// Gets the new best bid size + /// + public decimal BestBidSize { get; } + + /// + /// Gets the new best ask price + /// + public decimal BestAskPrice { get; } + + /// + /// Gets the new best ask size + /// + public decimal BestAskSize { get; } + + /// + /// Initializes a new instance of the class + /// + /// The symbol + /// The newly updated best bid price + /// >The newly updated best bid size + /// The newly updated best ask price + /// The newly updated best ask size + public BestBidAskUpdatedEventArgs(Symbol symbol, decimal bestBidPrice, decimal bestBidSize, decimal bestAskPrice, decimal bestAskSize) + { + Symbol = symbol; + BestBidPrice = bestBidPrice; + BestBidSize = bestBidSize; + BestAskPrice = bestAskPrice; + BestAskSize = bestAskSize; + } + } +} diff --git a/Brokerages/Bitfinex/BitfinexBrokerage.Messaging.cs b/Brokerages/Bitfinex/BitfinexBrokerage.Messaging.cs index 2ce156ac010c..c731bca27510 100644 --- a/Brokerages/Bitfinex/BitfinexBrokerage.Messaging.cs +++ b/Brokerages/Bitfinex/BitfinexBrokerage.Messaging.cs @@ -14,7 +14,9 @@ */ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -30,6 +32,10 @@ public partial class BitfinexBrokerage : BaseWebsocketsBrokerage, IDataQueueHand /// /// Wss message handler /// + /// + #region Declarations + private readonly ConcurrentDictionary _orderBooks = new ConcurrentDictionary(); + #endregion /// /// protected override void OnMessageImpl(object sender, WebSocketMessage e) @@ -49,6 +55,23 @@ protected override void OnMessageImpl(object sender, WebSocketMessage e) _lastHeartbeatUtcTime = DateTime.UtcNow; return; } + else if (ChannelList.ContainsKey(id) && ChannelList[id].Name == "book") + { + if (raw[1].Type == JTokenType.Array) + { + //order book snapshot + var data = raw[1].ToObject(typeof(string[][])); + PopulateOrderBook(data, ChannelList[id].Symbol); + return; + } + else + { + //order book update + var data = raw.ToObject(typeof(string[])); + L2Update(data, ChannelList[id].Symbol); + } + + } else if (ChannelList.ContainsKey(id) && ChannelList[id].Name == "ticker") { //ticker @@ -72,6 +95,39 @@ protected override void OnMessageImpl(object sender, WebSocketMessage e) PopulateTrade(term, data); return; } + else if (id == "0" && (term == "on" || term == "ou" || term == "oc")) + { + //order updates + Log.Trace("BitfinexBrokerage.OnMessage(): Order update"); + var data = raw[2].ToObject(typeof(string[])); + UpdateOrder(term, data); + return; + + } + else if (id == "0" && term == "os") + { + //order snapshot + Log.Trace("BitfinexBrokerage.OnMessage(): Order Snapshot"); + var data = raw[2].ToObject(typeof(string[][])); + PopulateOrder(data); + + } + else if (id == "0" && (term == "pn" || term == "pu" || term == "pc")) + { + //position updates + Log.Trace("BitfinexBrokerage.OnMessage(): Order update"); + var data = raw[2].ToObject(typeof(string[])); + return; + + } + else if (id == "0" && term == "ps") + { + //position snapshot + Log.Trace("BitfinexBrokerage.OnMessage(): Position Snapshot"); + //var data = raw[2].ToObject(typeof(string[][])); + return; + + } else if (term == "ws") { //wallet @@ -84,7 +140,7 @@ protected override void OnMessageImpl(object sender, WebSocketMessage e) return; } } - else if ((raw.channel == "ticker" || raw.channel == "trades") && raw.@event == "subscribed") + else if ((raw.channel == "ticker" || raw.channel == "trades" || raw.channel == "book") && raw.@event == "subscribed") { var channel = (string)raw.channel; var currentChannelId = (string)raw.chanId; @@ -119,7 +175,7 @@ protected override void OnMessageImpl(object sender, WebSocketMessage e) return; } - Log.Trace("BitfinexBrokerage.OnMessage(): " + e.Message); + //Log.Trace("BitfinexBrokerage.OnMessage(): " + e.Message); } catch (Exception ex) { @@ -128,6 +184,95 @@ protected override void OnMessageImpl(object sender, WebSocketMessage e) } } + private void PopulateOrderBook(string[][] data, string symbol) + { + OrderBook orderBook; + if (!_orderBooks.TryGetValue(symbol, out orderBook)) + { + orderBook = new OrderBook(symbol); + _orderBooks[symbol] = orderBook; + } + else + { + orderBook.BestBidAskUpdated -= OnBestBidAskUpdated; + orderBook.Clear(); + } + + foreach (var item in data) + { + var msg = new Messages.OrderBook(item); + // Positive values -> bid + if (msg.Price > 0) + { + orderBook.UpdateAskRow(msg.Price, msg.Amount); + } + // negative values -> ask. + else if (msg.Price < 0) + { + orderBook.UpdateBidRow(msg.Price, msg.Amount); + } + + orderBook.BestBidAskUpdated += OnBestBidAskUpdated; + } + return; + } + + private void OnBestBidAskUpdated(object sender, BestBidAskUpdatedEventArgs e) + { + EmitQuoteTick(e.Symbol, e.BestBidPrice, e.BestBidSize, e.BestAskPrice, e.BestAskSize); + } + + private void L2Update(string[] data, string symbol) + { + var orderBook = _orderBooks[symbol]; + + var msg = new Messages.L2Update(data); + + // Positive values -> bid + if (msg.Price > 0) + { + if (msg.Count == 0) + { + orderBook.RemoveAskRow(msg.Price); + } + else + { + orderBook.UpdateAskRow(msg.Price, msg.Amount); + } + } + // negative values -> ask. + else if (msg.Price < 0) + { + if (msg.Count == 0) + { + orderBook.RemoveBidRow(msg.Price); + } + else + { + orderBook.UpdateBidRow(msg.Price, msg.Amount); + } + } + return; + } + + private void EmitQuoteTick(Symbol symbol, decimal bidPrice, decimal bidSize, decimal askPrice, decimal askSize) + { + lock (Ticks) + { + Ticks.Add(new Tick + { + AskPrice = askPrice, + BidPrice = bidPrice, + Value = (askPrice + bidPrice) / 2m, + Time = DateTime.UtcNow, + Symbol = symbol, + TickType = TickType.Quote, + AskSize = askSize, + BidSize = bidSize + }); + } + } + private void PopulateTicker(string response, string symbol) { var data = JsonConvert.DeserializeObject(response, settings); @@ -168,6 +313,51 @@ private void PopulateTradeTicker(string response, string symbol) } } + private void UpdateOrder(string term, string[] data) + { + var msg = new Messages.OrderUpdate(data); + OrderDirection direction = msg.OrderAmount < 0 ? OrderDirection.Sell : OrderDirection.Buy; + Symbol symbol = Symbol.Create(msg.OrderPair.ToUpper(), SecurityType.Crypto, BrokerageMarket); + + Log.Trace(msg.ToString()); + // order new + if (term == "on") + { + // var orderEvent = new OrderEvent + //( + // orderId, symbol, DateTime.UtcNow, OrderStatus.New, + // direction, msg.OrderPrice, msg.OrderAmount, 0, "Bitfinex New Order Event" + //); + // OnOrderEvent(orderEvent); + return; + } + // todo - order update + else if (term == "ou") + { + return; + } + // order cancel + else if (term == "oc") + { + // var orderEvent = new OrderEvent + //( + // orderId, symbol, DateTime.UtcNow, OrderStatus.Canceled, + // direction, msg.OrderPrice, msg.OrderAmount, 0, "Bitfinex Cancel Order Event" + //); + //OnOrderEvent(orderEvent); + return; + } + } + + private void PopulateOrder(string[][] data) + { + foreach (var item in data) + { + var msg = new Messages.OrderUpdate(item); + Log.Trace(msg.ToString()); + } + } + private void PopulateWallet(string[][] data) { foreach (var item in data) @@ -303,6 +493,16 @@ public void Subscribe(Packets.LiveNodePacket job, IEnumerable symbols) pair = item.Value })); + WebSocket.Send(JsonConvert.SerializeObject(new + { + @event = "subscribe", + channel = "book", + pair = item.Value, + prec = "P0", // default P0 + freq = "F0", // default F0 + length = "5" // default 25 + })); + Log.Trace("BitfinexBrokerage.Subscribe(): Sent subcribe for " + item.Value); } } diff --git a/Brokerages/Bitfinex/Messages/L2Update.cs b/Brokerages/Bitfinex/Messages/L2Update.cs new file mode 100644 index 000000000000..57b999a404fe --- /dev/null +++ b/Brokerages/Bitfinex/Messages/L2Update.cs @@ -0,0 +1,61 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +namespace QuantConnect.Brokerages.Bitfinex.Messages +{ + /// + /// Ticker message object + /// + public class L2Update : BaseMessage + { + private const int _channelId = 0; + private const int _price = 1; + private const int _count = 2; + private const int _amount = 3; + + /// + /// L2Update Message constructor + /// + /// + public L2Update(string[] values) + : base(values) + { + ChannelId = GetInt(_channelId); + Price = TryGetDecimal(_price); + Count = GetInt(_count); + Amount = TryGetDecimal(_amount); + } + + /// + /// Channel Id + /// + public int ChannelId { get; set; } + + /// + /// Price + /// + public decimal Price { get; set; } + + /// + /// Count + /// + public decimal Count { get; set; } + + /// + /// Amount + /// + public decimal Amount { get; set; } + } +} \ No newline at end of file diff --git a/Brokerages/Bitfinex/Messages/OrderBook.cs b/Brokerages/Bitfinex/Messages/OrderBook.cs new file mode 100644 index 000000000000..9110a5ce5389 --- /dev/null +++ b/Brokerages/Bitfinex/Messages/OrderBook.cs @@ -0,0 +1,54 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +namespace QuantConnect.Brokerages.Bitfinex.Messages +{ + /// + /// Ticker message object + /// + public class OrderBook : BaseMessage + { + private const int _price = 0; + private const int _count = 1; + private const int _amount = 2; + + /// + /// Ticker Message constructor + /// + /// + public OrderBook(string[] values) + : base(values) + { + Price = TryGetDecimal(_price); + Count = GetInt(_count); + Amount = TryGetDecimal(_amount); + } + + /// + /// Price + /// + public decimal Price { get; set; } + + /// + /// Count + /// + public decimal Count { get; set; } + + /// + /// Amount + /// + public decimal Amount { get; set; } + } +} \ No newline at end of file diff --git a/Brokerages/Bitfinex/Messages/OrderUpdate.cs b/Brokerages/Bitfinex/Messages/OrderUpdate.cs new file mode 100644 index 000000000000..86915e3f6e20 --- /dev/null +++ b/Brokerages/Bitfinex/Messages/OrderUpdate.cs @@ -0,0 +1,98 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ +using System; + +namespace QuantConnect.Brokerages.Bitfinex.Messages +{ + /// + /// Ticker message object + /// + public class OrderUpdate : BaseMessage + { + private const int _id = 0; + private const int _pair = 1; + private const int _amount = 2; + private const int _amount_orig = 3; + private const int _type = 4; + private const int _status = 5; + private const int _price = 6; + private const int _price_avg = 7; + private const int _created_at = 8; + private const int _notify = 9; + private const int _hidden = 10; + private const int _oco = 11; + + /// + /// L2Update Message constructor + /// + /// + public OrderUpdate(string[] values) + : base(values) + { + OrderId = GetLong(_id); + OrderPair = GetString(_pair); + OrderAmount = TryGetDecimal(_id); + //OrderAmountOrig = + OrderType = GetString(_type); + OrderStatus = GetString(_status); + OrderPrice = TryGetDecimal(_price); + OrderPriceAvg = TryGetDecimal(_price_avg); + OrderCreatedAt = GetString(_created_at); + //OrderNotify = GetString(_notify); + //OrderHidden = GetInt(_hidden); + //OrderOco = GetInt(_oco); + } + + /// + /// Order Id + /// + public long OrderId { get; set; } + + /// + /// Order Pair + /// + public string OrderPair { get; set; } + + /// + /// Order Amount + /// + public decimal OrderAmount { get; set; } + + /// + /// Order Type + /// + public string OrderType { get; set; } + + /// + /// Order Status + /// + public string OrderStatus { get; set; } + + /// + /// Order Price + /// + public decimal OrderPrice { get; set; } + + /// + /// Order Price Avg + /// + public decimal OrderPriceAvg { get; set; } + + /// + /// Order Created At + /// + public string OrderCreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/Brokerages/Bitfinex/OrderBook.cs b/Brokerages/Bitfinex/OrderBook.cs new file mode 100644 index 000000000000..e90cea94e493 --- /dev/null +++ b/Brokerages/Bitfinex/OrderBook.cs @@ -0,0 +1,173 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace QuantConnect.Brokerages.Bitfinex +{ + /// + /// Represents a full order book for a security. + /// It contains prices and order sizes for each bid and ask level. + /// The best bid and ask prices are also kept up to date. + /// + public class OrderBook + { + private readonly object _locker = new object(); + private readonly Symbol _symbol; + private readonly SortedDictionary _bids = new SortedDictionary(); + private readonly SortedDictionary _asks = new SortedDictionary(); + + /// + /// Event fired each time or are changed + /// + public event EventHandler BestBidAskUpdated; + + /// + /// The best bid price + /// + public decimal BestBidPrice { get; private set; } + + /// + /// The best bid size + /// + public decimal BestBidSize { get; private set; } + + /// + /// The best ask price + /// + public decimal BestAskPrice { get; private set; } + + /// + /// The best ask size + /// + public decimal BestAskSize { get; private set; } + + /// + /// Initializes a new instance of the class + /// + /// The symbol for the order book + public OrderBook(Symbol symbol) + { + _symbol = symbol; + } + + /// + /// Clears all bid/ask levels and prices. + /// + public void Clear() + { + lock (_locker) + { + _bids.Clear(); + _asks.Clear(); + } + + BestBidPrice = 0; + BestBidSize = 0; + BestAskPrice = 0; + BestAskSize = 0; + } + + /// + /// Updates or inserts a bid price level in the order book + /// + /// The bid price level to be inserted or updated + /// The new size at the bid price level + public void UpdateBidRow(decimal price, decimal size) + { + lock(_locker) + { + _bids[price] = size; + } + + if (BestBidPrice == 0 || price >= BestBidPrice) + { + BestBidPrice = price; + BestBidSize = size; + + BestBidAskUpdated?.Invoke(this, new BestBidAskUpdatedEventArgs(_symbol, BestBidPrice, BestBidSize, BestAskPrice, BestAskSize)); + } + } + + /// + /// Updates or inserts an ask price level in the order book + /// + /// The ask price level to be inserted or updated + /// The new size at the ask price level + public void UpdateAskRow(decimal price, decimal size) + { + lock(_locker) + { + _asks[price] = size; + } + + if (BestAskPrice == 0 || price <= BestAskPrice) + { + BestAskPrice = price; + BestAskSize = size; + + BestBidAskUpdated?.Invoke(this, new BestBidAskUpdatedEventArgs(_symbol, BestBidPrice, BestBidSize, BestAskPrice, BestAskSize)); + } + } + + /// + /// Removes a bid price level from the order book + /// + /// The bid price level to be removed + public void RemoveBidRow(decimal price) + { + lock(_locker) + { + _bids.Remove(price); + } + + if (price == BestBidPrice) + { + lock(_locker) + { + BestBidPrice = _bids.Keys.LastOrDefault(); + BestBidSize = BestBidPrice > 0 ? _bids[BestBidPrice] : 0; + } + + BestBidAskUpdated?.Invoke(this, new BestBidAskUpdatedEventArgs(_symbol, BestBidPrice, BestBidSize, BestAskPrice, BestAskSize)); + } + } + + /// + /// Removes an ask price level from the order book + /// + /// The ask price level to be removed + public void RemoveAskRow(decimal price) + { + lock(_locker) + { + _asks.Remove(price); + } + + if (price == BestAskPrice) + { + lock(_locker) + { + BestAskPrice = _asks.Keys.FirstOrDefault(); + BestAskSize = BestAskPrice > 0 ? _asks[BestAskPrice] : 0; + } + + BestBidAskUpdated?.Invoke(this, new BestBidAskUpdatedEventArgs(_symbol, BestBidPrice, BestBidSize, BestAskPrice, BestAskSize)); + } + } + } +} diff --git a/Brokerages/QuantConnect.Brokerages.csproj b/Brokerages/QuantConnect.Brokerages.csproj index 85183df8a3ef..29a2bce1d7c9 100644 --- a/Brokerages/QuantConnect.Brokerages.csproj +++ b/Brokerages/QuantConnect.Brokerages.csproj @@ -171,12 +171,16 @@ + + + + diff --git a/Tests/Brokerages/Bitfinex/BitfinexBrokerageIntegrationTests.cs b/Tests/Brokerages/Bitfinex/BitfinexBrokerageIntegrationTests.cs index c9b7d8fa8369..1539c8a5a348 100644 --- a/Tests/Brokerages/Bitfinex/BitfinexBrokerageIntegrationTests.cs +++ b/Tests/Brokerages/Bitfinex/BitfinexBrokerageIntegrationTests.cs @@ -23,7 +23,7 @@ public class BitfinexBrokerageIntegrationTests : BitfinexBrokerageIntegrationTes { #region Properties - protected override Symbol Symbol => Symbol.Create("BTCUSD", SecurityType.Forex, Market.Bitfinex); + protected override Symbol Symbol => Symbol.Create("BTCUSD", SecurityType.Crypto, Market.Bitfinex); /// /// Gets a high price for the specified symbol so a limit sell won't fill diff --git a/Tests/Brokerages/Bitfinex/BitfinexBrokerageIntegrationTestsBase.cs b/Tests/Brokerages/Bitfinex/BitfinexBrokerageIntegrationTestsBase.cs index ab79c00eb9cd..756cd7bc2eed 100644 --- a/Tests/Brokerages/Bitfinex/BitfinexBrokerageIntegrationTestsBase.cs +++ b/Tests/Brokerages/Bitfinex/BitfinexBrokerageIntegrationTestsBase.cs @@ -57,13 +57,13 @@ protected override decimal GetDefaultQuantity() protected override IBrokerage CreateBrokerage(IOrderProvider orderProvider, ISecurityProvider securityProvider) { - var restClient = new RestClient("https://api.gdax.com"); + var restClient = new RestClient("https://api.bitfinex.com"); var webSocketClient = new WebSocketWrapper(); var algorithm = new Mock(); algorithm.Setup(a => a.BrokerageModel).Returns(new BitfinexBrokerageModel(AccountType.Cash)); - return new BitfinexBrokerage(Config.Get("gdax-url", "wss://ws-feed.gdax.com"), webSocketClient, restClient, Config.Get("bitfinex-api-key"), Config.Get("bitfinex-api-secret"), + return new BitfinexBrokerage(Config.Get("bitfinex-wss", "wss://api2.bitfinex.com:3000/ws"), webSocketClient, restClient, Config.Get("bitfinex-api-key"), Config.Get("bitfinex-api-secret"), algorithm.Object); } diff --git a/Tests/Brokerages/Bitfinex/BitfinexBrokerageWebsocketsTests.cs b/Tests/Brokerages/Bitfinex/BitfinexBrokerageWebsocketsTests.cs index 78c242cf4b63..b358ed96f62f 100644 --- a/Tests/Brokerages/Bitfinex/BitfinexBrokerageWebsocketsTests.cs +++ b/Tests/Brokerages/Bitfinex/BitfinexBrokerageWebsocketsTests.cs @@ -367,10 +367,11 @@ public void SubscribeTest() _unit.Subscribe(new[] { Symbol.Create("BTCUSD", SecurityType.Crypto, Market.Bitfinex), Symbol.Create("UNIVERSE", SecurityType.Crypto, Market.Bitfinex), Symbol.Create("ETHBTC", SecurityType.Crypto, Market.Bitfinex)}); - Assert.AreEqual(4, actualSymbols.Count); - Assert.AreEqual(4, actualChannels.Count); + Assert.AreEqual(6, actualSymbols.Count); + Assert.AreEqual(6, actualChannels.Count); CollectionAssert.Contains(actualChannels, "ticker"); CollectionAssert.Contains(actualChannels, "trades"); + CollectionAssert.Contains(actualChannels, "book"); CollectionAssert.Contains(actualSymbols, "BTCUSD"); CollectionAssert.Contains(actualSymbols, "ETHBTC"); }