BitfinexBrokerage updates (#5787)
* Update BinanceBrokerage to handle more than 512 symbols * Address review - fetch symbol weights only if required - remove code duplication * Add rate limiting for new connections * Update WebSocketMessage to include the websocket instance * Handle resubscriptions on reconnect * Address review * Address review * Remove unnecessary locking * WebSocketClientWrapper updates - remove allocation of receive buffer on each message - add missing lock in Close method - log message data when message type is Close - fix race condition after unexpected websocket close * Set WebSocketClientWrapper task to LongRunning * Add missing check in GetHistory * Fix exceptions with Binance downloader - closes #5794 * Update Bitfinex symbols in symbol properties database * Update BitfinexBrokerage to use BrokerageMultiWebSocketSubscriptionManager * Address review * Remove unnecessary locking * Remove old channels on resubscription
This commit is contained in:
@@ -35,7 +35,7 @@ namespace QuantConnect.Brokerages.Binance
|
||||
/// </summary>
|
||||
protected readonly object TickLocker = new object();
|
||||
|
||||
private void OnMessageImpl(WebSocketMessage e)
|
||||
private void OnUserMessage(WebSocketMessage e)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -65,7 +65,39 @@ namespace QuantConnect.Brokerages.Binance
|
||||
OnFillOrder(upd);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, -1, $"Parsing wss message failed. Data: {e.Message} Exception: {exception}"));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDataMessage(WebSocketMessage e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var obj = JObject.Parse(e.Message);
|
||||
|
||||
var objError = obj["error"];
|
||||
if (objError != null)
|
||||
{
|
||||
var error = objError.ToObject<ErrorMessage>();
|
||||
OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, error.Code, error.Message));
|
||||
return;
|
||||
}
|
||||
|
||||
var objData = obj;
|
||||
|
||||
var objEventType = objData["e"];
|
||||
if (objEventType != null)
|
||||
{
|
||||
var eventType = objEventType.ToObject<string>();
|
||||
|
||||
switch (eventType)
|
||||
{
|
||||
case "trade":
|
||||
var trade = objData.ToObject<Trade>();
|
||||
EmitTradeTick(
|
||||
|
||||
@@ -25,6 +25,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using QuantConnect.Configuration;
|
||||
using QuantConnect.Util;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
@@ -52,6 +54,8 @@ namespace QuantConnect.Brokerages.Binance
|
||||
private readonly BinanceRestApiClient _apiClient;
|
||||
private readonly BrokerageConcurrentMessageHandler<WebSocketMessage> _messageHandler;
|
||||
|
||||
private const int MaximumSymbolsPerConnection = 512;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for brokerage
|
||||
/// </summary>
|
||||
@@ -66,15 +70,20 @@ namespace QuantConnect.Brokerages.Binance
|
||||
_job = job;
|
||||
_algorithm = algorithm;
|
||||
_aggregator = aggregator;
|
||||
_messageHandler = new BrokerageConcurrentMessageHandler<WebSocketMessage>(OnMessageImpl);
|
||||
_messageHandler = new BrokerageConcurrentMessageHandler<WebSocketMessage>(OnUserMessage);
|
||||
|
||||
var subscriptionManager = new EventBasedDataQueueHandlerSubscriptionManager();
|
||||
subscriptionManager.SubscribeImpl += (s, t) =>
|
||||
{
|
||||
Subscribe(s);
|
||||
return true;
|
||||
};
|
||||
subscriptionManager.UnsubscribeImpl += (s, t) => Unsubscribe(s);
|
||||
var maximumWebSocketConnections = Config.GetInt("binance-maximum-websocket-connections");
|
||||
var symbolWeights = maximumWebSocketConnections > 0 ? FetchSymbolWeights() : null;
|
||||
|
||||
var subscriptionManager = new BrokerageMultiWebSocketSubscriptionManager(
|
||||
WebSocketBaseUrl,
|
||||
MaximumSymbolsPerConnection,
|
||||
maximumWebSocketConnections,
|
||||
symbolWeights,
|
||||
() => new BinanceWebSocketWrapper(null),
|
||||
Subscribe,
|
||||
Unsubscribe,
|
||||
OnDataMessage);
|
||||
|
||||
SubscriptionManager = subscriptionManager;
|
||||
|
||||
@@ -398,51 +407,55 @@ namespace QuantConnect.Brokerages.Binance
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the requested symbols (using an individual streaming channel)
|
||||
/// Not used
|
||||
/// </summary>
|
||||
/// <param name="symbols">The list of symbols to subscribe</param>
|
||||
public override void Subscribe(IEnumerable<Symbol> symbols)
|
||||
{
|
||||
foreach (var symbol in symbols)
|
||||
{
|
||||
Send(WebSocket,
|
||||
new
|
||||
{
|
||||
method = "SUBSCRIBE",
|
||||
@params = new[]
|
||||
{
|
||||
$"{symbol.Value.ToLowerInvariant()}@trade",
|
||||
$"{symbol.Value.ToLowerInvariant()}@bookTicker"
|
||||
},
|
||||
id = GetNextRequestId()
|
||||
}
|
||||
);
|
||||
}
|
||||
// NOP
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends current subscriptions
|
||||
/// Subscribes to the requested symbol (using an individual streaming channel)
|
||||
/// </summary>
|
||||
private bool Unsubscribe(IEnumerable<Symbol> symbols)
|
||||
/// <param name="webSocket">The websocket instance</param>
|
||||
/// <param name="symbol">The symbol to subscribe</param>
|
||||
private bool Subscribe(IWebSocket webSocket, Symbol symbol)
|
||||
{
|
||||
if (WebSocket.IsOpen)
|
||||
{
|
||||
foreach (var symbol in symbols)
|
||||
Send(webSocket,
|
||||
new
|
||||
{
|
||||
Send(WebSocket,
|
||||
new
|
||||
{
|
||||
method = "UNSUBSCRIBE",
|
||||
@params = new[]
|
||||
{
|
||||
$"{symbol.Value.ToLowerInvariant()}@trade",
|
||||
$"{symbol.Value.ToLowerInvariant()}@bookTicker"
|
||||
},
|
||||
id = GetNextRequestId()
|
||||
}
|
||||
);
|
||||
method = "SUBSCRIBE",
|
||||
@params = new[]
|
||||
{
|
||||
$"{symbol.Value.ToLowerInvariant()}@trade",
|
||||
$"{symbol.Value.ToLowerInvariant()}@bookTicker"
|
||||
},
|
||||
id = GetNextRequestId()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends current subscription
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The websocket instance</param>
|
||||
/// <param name="symbol">The symbol to unsubscribe</param>
|
||||
private bool Unsubscribe(IWebSocket webSocket, Symbol symbol)
|
||||
{
|
||||
Send(webSocket,
|
||||
new
|
||||
{
|
||||
method = "UNSUBSCRIBE",
|
||||
@params = new[]
|
||||
{
|
||||
$"{symbol.Value.ToLowerInvariant()}@trade",
|
||||
$"{symbol.Value.ToLowerInvariant()}@bookTicker"
|
||||
},
|
||||
id = GetNextRequestId()
|
||||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -485,5 +498,36 @@ namespace QuantConnect.Brokerages.Binance
|
||||
CachedOrderIDs.TryAdd(order.Id, order);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the weights for each symbol (the weight value is the count of trades in the last 24 hours)
|
||||
/// </summary>
|
||||
private static Dictionary<Symbol, int> FetchSymbolWeights()
|
||||
{
|
||||
var dict = new Dictionary<Symbol, int>();
|
||||
|
||||
try
|
||||
{
|
||||
const string url = "https://api.binance.com/api/v3/ticker/24hr";
|
||||
var json = url.DownloadData();
|
||||
|
||||
foreach (var row in JArray.Parse(json))
|
||||
{
|
||||
var ticker = row["symbol"].ToObject<string>();
|
||||
var count = row["count"].ToObject<int>();
|
||||
|
||||
var symbol = Symbol.Create(ticker, SecurityType.Crypto, Market.Binance);
|
||||
|
||||
dict.Add(symbol, count);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error(exception);
|
||||
throw;
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
*
|
||||
@@ -317,12 +317,18 @@ namespace QuantConnect.Brokerages.Binance
|
||||
var klines = JsonConvert.DeserializeObject<object[][]>(response.Content)
|
||||
.Select(entries => new Messages.Kline(entries))
|
||||
.ToList();
|
||||
|
||||
startMs = klines.Last().OpenTime + resolutionInMs;
|
||||
|
||||
foreach (var kline in klines)
|
||||
if (klines.Count > 0)
|
||||
{
|
||||
yield return kline;
|
||||
startMs = klines.Last().OpenTime + resolutionInMs;
|
||||
|
||||
foreach (var kline in klines)
|
||||
{
|
||||
yield return kline;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
startMs += resolutionInMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,19 +361,17 @@ namespace QuantConnect.Brokerages.Binance
|
||||
/// </summary>
|
||||
public void StopSession()
|
||||
{
|
||||
if (string.IsNullOrEmpty(SessionId))
|
||||
if (!string.IsNullOrEmpty(SessionId))
|
||||
{
|
||||
throw new Exception("BinanceBrokerage:UserStream. listenKey wasn't allocated or has been refused.");
|
||||
var request = new RestRequest(UserDataStreamEndpoint, Method.DELETE);
|
||||
request.AddHeader(KeyHeader, ApiKey);
|
||||
request.AddParameter(
|
||||
"application/x-www-form-urlencoded",
|
||||
Encoding.UTF8.GetBytes($"listenKey={SessionId}"),
|
||||
ParameterType.RequestBody
|
||||
);
|
||||
ExecuteRestRequest(request);
|
||||
}
|
||||
|
||||
var request = new RestRequest(UserDataStreamEndpoint, Method.DELETE);
|
||||
request.AddHeader(KeyHeader, ApiKey);
|
||||
request.AddParameter(
|
||||
"application/x-www-form-urlencoded",
|
||||
Encoding.UTF8.GetBytes($"listenKey={SessionId}"),
|
||||
ParameterType.RequestBody
|
||||
);
|
||||
ExecuteRestRequest(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
525
Brokerages/Bitfinex/BitfinexBrokerage.DataQueueHandler.cs
Normal file
525
Brokerages/Bitfinex/BitfinexBrokerage.DataQueueHandler.cs
Normal file
@@ -0,0 +1,525 @@
|
||||
/*
|
||||
* 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.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using QuantConnect.Brokerages.Bitfinex.Messages;
|
||||
using QuantConnect.Data;
|
||||
using QuantConnect.Data.Market;
|
||||
using QuantConnect.Logging;
|
||||
using QuantConnect.Util;
|
||||
|
||||
namespace QuantConnect.Brokerages.Bitfinex
|
||||
{
|
||||
public partial class BitfinexBrokerage
|
||||
{
|
||||
private readonly RateGate _connectionRateLimiter = new(5, TimeSpan.FromMinutes(1));
|
||||
private readonly ConcurrentDictionary<IWebSocket, BitfinexWebSocketChannels> _channelsByWebSocket = new();
|
||||
private readonly ConcurrentDictionary<Symbol, DefaultOrderBook> _orderBooks = new();
|
||||
private readonly ManualResetEvent _onSubscribeEvent = new(false);
|
||||
private readonly ManualResetEvent _onUnsubscribeEvent = new(false);
|
||||
private readonly object _locker = new();
|
||||
|
||||
private const int MaximumSymbolsPerConnection = 12;
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the requested symbol (using an individual streaming channel)
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The websocket instance</param>
|
||||
/// <param name="symbol">The symbol to subscribe</param>
|
||||
private bool Subscribe(IWebSocket webSocket, Symbol symbol)
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
if (!_channelsByWebSocket.ContainsKey(webSocket))
|
||||
{
|
||||
_channelsByWebSocket.TryAdd(webSocket, new BitfinexWebSocketChannels());
|
||||
}
|
||||
}
|
||||
|
||||
var success = SubscribeChannel(webSocket, "trades", symbol);
|
||||
success &= SubscribeChannel(webSocket, "book", symbol);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends current subscription
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The websocket instance</param>
|
||||
/// <param name="symbol">The symbol to unsubscribe</param>
|
||||
private bool Unsubscribe(IWebSocket webSocket, Symbol symbol)
|
||||
{
|
||||
BitfinexWebSocketChannels channels;
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
if (!_channelsByWebSocket.TryGetValue(webSocket, out channels))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var success = UnsubscribeChannel(webSocket, channels, new Channel("trades", symbol));
|
||||
success &= UnsubscribeChannel(webSocket, channels, new Channel("book", symbol));
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private bool SubscribeChannel(IWebSocket webSocket, string channelName, Symbol symbol)
|
||||
{
|
||||
_onSubscribeEvent.Reset();
|
||||
|
||||
webSocket.Send(JsonConvert.SerializeObject(new
|
||||
{
|
||||
@event = "subscribe",
|
||||
channel = channelName,
|
||||
pair = _symbolMapper.GetBrokerageSymbol(symbol)
|
||||
}));
|
||||
|
||||
if (!_onSubscribeEvent.WaitOne(TimeSpan.FromSeconds(30)))
|
||||
{
|
||||
Log.Error($"BitfinexBrokerage.Unsubscribe(): Could not subscribe to {symbol.Value}/{channelName}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool UnsubscribeChannel(IWebSocket webSocket, BitfinexWebSocketChannels channels, Channel channel)
|
||||
{
|
||||
if (channels.Contains(channel))
|
||||
{
|
||||
var channelId = channels.GetChannelId(channel);
|
||||
|
||||
_onUnsubscribeEvent.Reset();
|
||||
|
||||
webSocket.Send(JsonConvert.SerializeObject(new
|
||||
{
|
||||
@event = "unsubscribe",
|
||||
chanId = channelId.ToStringInvariant()
|
||||
}));
|
||||
|
||||
if (!_onUnsubscribeEvent.WaitOne(TimeSpan.FromSeconds(30)))
|
||||
{
|
||||
Log.Error($"BitfinexBrokerage.Unsubscribe(): Could not unsubscribe from {channel.Symbol.Value}/{channel.Name}.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnDataMessage(WebSocketMessage e)
|
||||
{
|
||||
var webSocket = (BitfinexWebSocketWrapper)e.WebSocket;
|
||||
|
||||
try
|
||||
{
|
||||
var token = JToken.Parse(e.Message);
|
||||
|
||||
if (token is JArray)
|
||||
{
|
||||
var channel = token[0].ToObject<int>();
|
||||
|
||||
if (token[1].Type == JTokenType.String)
|
||||
{
|
||||
var type = token[1].Value<string>();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
// heartbeat
|
||||
case "hb":
|
||||
return;
|
||||
|
||||
// trade execution
|
||||
case "te":
|
||||
OnUpdate(webSocket, channel, token[2].ToObject<string[]>());
|
||||
break;
|
||||
|
||||
// ignored -- trades already handled in "te" message
|
||||
// https://github.com/bitfinexcom/bitfinex-api-node#te-vs-tu-messages
|
||||
case "tu":
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.Error($"BitfinexBrokerage.OnDataMessage(): Unexpected message type: {type}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// public channels
|
||||
else if (channel != 0 && token[1].Type == JTokenType.Array)
|
||||
{
|
||||
var tokens = (JArray)token[1];
|
||||
|
||||
if (tokens.Count > 0)
|
||||
{
|
||||
if (tokens[0].Type == JTokenType.Array)
|
||||
{
|
||||
OnSnapshot(
|
||||
webSocket,
|
||||
channel,
|
||||
tokens.ToObject<string[][]>()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// pass channel id as separate arg
|
||||
OnUpdate(
|
||||
webSocket,
|
||||
channel,
|
||||
tokens.ToObject<string[]>()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (token is JObject)
|
||||
{
|
||||
var raw = token.ToObject<BaseMessage>();
|
||||
switch (raw.Event.ToLowerInvariant())
|
||||
{
|
||||
case "subscribed":
|
||||
OnSubscribe(webSocket, token.ToObject<ChannelSubscription>());
|
||||
return;
|
||||
|
||||
case "unsubscribed":
|
||||
OnUnsubscribe(webSocket, token.ToObject<ChannelUnsubscribing>());
|
||||
return;
|
||||
|
||||
case "auth":
|
||||
case "info":
|
||||
case "ping":
|
||||
return;
|
||||
|
||||
case "error":
|
||||
var error = token.ToObject<ErrorMessage>();
|
||||
// 10300 Subscription failed (generic) | 10301 : Already subscribed | 10302 : Unknown channel
|
||||
// see https://docs.bitfinex.com/docs/ws-general
|
||||
if (error.Code == 10300 || error.Code == 10301 || error.Code == 10302)
|
||||
{
|
||||
//_subscribeErrorCode = error.Code;
|
||||
_onSubscribeEvent.Set();
|
||||
}
|
||||
Log.Error($"BitfinexBrokerage.OnDataMessage(): {e.Message}");
|
||||
return;
|
||||
|
||||
default:
|
||||
Log.Error($"BitfinexBrokerage.OnDataMessage(): Unexpected message format: {e.Message}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, -1, $"Parsing wss message failed. Data: {e.Message} Exception: {exception}"));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSubscribe(BitfinexWebSocketWrapper webSocket, ChannelSubscription data)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
var symbol = _symbolMapper.GetLeanSymbol(data.Symbol, SecurityType.Crypto, Market.Bitfinex);
|
||||
var channel = new Channel(data.Channel, symbol);
|
||||
|
||||
if (!_channelsByWebSocket.TryGetValue(webSocket, out var channels))
|
||||
{
|
||||
_onSubscribeEvent.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
// we need to update the channel on re subscription
|
||||
var channelsToRemove = channels
|
||||
.Where(x => x.Value.Equals(channel))
|
||||
.Select(x => x.Key)
|
||||
.ToList();
|
||||
foreach (var channelId in channelsToRemove)
|
||||
{
|
||||
channels.TryRemove(channelId, out _);
|
||||
}
|
||||
channels.AddOrUpdate(data.ChannelId, channel);
|
||||
|
||||
Log.Trace($"BitfinexBrokerage.OnSubscribe(): Channel subscribed: Id:{data.ChannelId} {channel.Symbol}/{channel.Name}");
|
||||
|
||||
_onSubscribeEvent.Set();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUnsubscribe(BitfinexWebSocketWrapper webSocket, ChannelUnsubscribing data)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
if (!_channelsByWebSocket.TryGetValue(webSocket, out var channels))
|
||||
{
|
||||
_onUnsubscribeEvent.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channels.TryRemove(data.ChannelId, out _))
|
||||
{
|
||||
_onUnsubscribeEvent.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
_onUnsubscribeEvent.Set();
|
||||
|
||||
if (channels.Count != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_channelsByWebSocket.TryRemove(webSocket, out channels);
|
||||
}
|
||||
|
||||
webSocket.Close();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSnapshot(BitfinexWebSocketWrapper webSocket, int channelId, string[][] entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
Channel channel;
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
if (!_channelsByWebSocket.TryGetValue(webSocket, out var channels))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channels.TryGetValue(channelId, out channel))
|
||||
{
|
||||
OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, -1, $"Message received from unknown channel Id {channelId}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (channel.Name.ToLowerInvariant())
|
||||
{
|
||||
case "book":
|
||||
ProcessOrderBookSnapshot(channel, entries);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessOrderBookSnapshot(Channel channel, string[][] entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
var symbol = channel.Symbol;
|
||||
|
||||
if (!_orderBooks.TryGetValue(symbol, out var orderBook))
|
||||
{
|
||||
orderBook = new DefaultOrderBook(symbol);
|
||||
_orderBooks[symbol] = orderBook;
|
||||
}
|
||||
else
|
||||
{
|
||||
orderBook.BestBidAskUpdated -= OnBestBidAskUpdated;
|
||||
orderBook.Clear();
|
||||
}
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var price = decimal.Parse(entry[0], NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
var amount = decimal.Parse(entry[2], NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
|
||||
if (amount > 0)
|
||||
{
|
||||
orderBook.UpdateBidRow(price, amount);
|
||||
}
|
||||
else
|
||||
{
|
||||
orderBook.UpdateAskRow(price, Math.Abs(amount));
|
||||
}
|
||||
}
|
||||
|
||||
orderBook.BestBidAskUpdated += OnBestBidAskUpdated;
|
||||
|
||||
EmitQuoteTick(symbol, orderBook.BestBidPrice, orderBook.BestBidSize, orderBook.BestAskPrice, orderBook.BestAskSize);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUpdate(BitfinexWebSocketWrapper webSocket, int channelId, string[] entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
Channel channel;
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
if (!_channelsByWebSocket.TryGetValue(webSocket, out var channels))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channels.TryGetValue(channelId, out channel))
|
||||
{
|
||||
OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, -1, $"Message received from unknown channel Id {channelId}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (channel.Name.ToLowerInvariant())
|
||||
{
|
||||
case "book":
|
||||
ProcessOrderBookUpdate(channel, entries);
|
||||
return;
|
||||
|
||||
case "trades":
|
||||
ProcessTradeUpdate(channel, entries);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessOrderBookUpdate(Channel channel, string[] entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
var symbol = channel.Symbol;
|
||||
var orderBook = _orderBooks[symbol];
|
||||
|
||||
var price = decimal.Parse(entries[0], NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
var count = Parse.Long(entries[1]);
|
||||
var amount = decimal.Parse(entries[2], NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
orderBook.RemovePriceLevel(price);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (amount > 0)
|
||||
{
|
||||
orderBook.UpdateBidRow(price, amount);
|
||||
}
|
||||
else if (amount < 0)
|
||||
{
|
||||
orderBook.UpdateAskRow(price, Math.Abs(amount));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Entries: [{string.Join(",", entries)}]");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessTradeUpdate(Channel channel, string[] entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
var time = Time.UnixMillisecondTimeStampToDateTime(decimal.Parse(entries[1], NumberStyles.Float, CultureInfo.InvariantCulture));
|
||||
var amount = decimal.Parse(entries[2], NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
var price = decimal.Parse(entries[3], NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
|
||||
EmitTradeTick(channel.Symbol, time, price, amount);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitTradeTick(Symbol symbol, DateTime time, decimal price, decimal amount)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (TickLocker)
|
||||
{
|
||||
EmitTick(new Tick
|
||||
{
|
||||
Value = price,
|
||||
Time = time,
|
||||
Symbol = symbol,
|
||||
TickType = TickType.Trade,
|
||||
Quantity = Math.Abs(amount)
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitQuoteTick(Symbol symbol, decimal bidPrice, decimal bidSize, decimal askPrice, decimal askSize)
|
||||
{
|
||||
lock (TickLocker)
|
||||
{
|
||||
EmitTick(new Tick
|
||||
{
|
||||
AskPrice = askPrice,
|
||||
BidPrice = bidPrice,
|
||||
Value = (askPrice + bidPrice) / 2m,
|
||||
Time = DateTime.UtcNow,
|
||||
Symbol = symbol,
|
||||
TickType = TickType.Quote,
|
||||
AskSize = Math.Abs(askSize),
|
||||
BidSize = Math.Abs(bidSize)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBestBidAskUpdated(object sender, BestBidAskUpdatedEventArgs e)
|
||||
{
|
||||
EmitQuoteTick(e.Symbol, e.BestBidPrice, e.BestBidSize, e.BestAskPrice, e.BestAskSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,18 @@ namespace QuantConnect.Brokerages.Bitfinex
|
||||
: base(WebSocketUrl, websocket, restClient, apiKey, apiSecret, "Bitfinex")
|
||||
{
|
||||
_job = job;
|
||||
SubscriptionManager = new BitfinexSubscriptionManager(this, WebSocketUrl, _symbolMapper);
|
||||
|
||||
SubscriptionManager = new BrokerageMultiWebSocketSubscriptionManager(
|
||||
WebSocketUrl,
|
||||
MaximumSymbolsPerConnection,
|
||||
0,
|
||||
null,
|
||||
() => new BitfinexWebSocketWrapper(null),
|
||||
Subscribe,
|
||||
Unsubscribe,
|
||||
OnDataMessage,
|
||||
_connectionRateLimiter);
|
||||
|
||||
_symbolPropertiesDatabase = SymbolPropertiesDatabase.FromDataFolder();
|
||||
_algorithm = algorithm;
|
||||
_aggregator = aggregator;
|
||||
@@ -147,7 +158,7 @@ namespace QuantConnect.Brokerages.Bitfinex
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should be empty, Bitfinex brokerage manages his public channels including subscribe/unsubscribe/reconnect methods using <see cref="BitfinexSubscriptionManager"/>
|
||||
/// Should be empty, Bitfinex brokerage manages his public channels including subscribe/unsubscribe/reconnect methods using <see cref="BrokerageMultiWebSocketSubscriptionManager"/>
|
||||
/// Not used in master
|
||||
/// </summary>
|
||||
/// <param name="symbols"></param>
|
||||
@@ -248,8 +259,8 @@ namespace QuantConnect.Brokerages.Bitfinex
|
||||
{
|
||||
case "auth":
|
||||
var auth = token.ToObject<AuthResponseMessage>();
|
||||
var result = string.Equals(auth.Status, "OK", StringComparison.OrdinalIgnoreCase) ? "succeed" : "failed";
|
||||
Log.Trace($"BitfinexWebsocketsBrokerage.OnMessage: Subscribing to authenticated channels {result}");
|
||||
var result = string.Equals(auth.Status, "OK", StringComparison.OrdinalIgnoreCase) ? "successful" : "failed";
|
||||
Log.Trace($"BitfinexBrokerage.OnMessage: Subscribing to authenticated channels {result}");
|
||||
return;
|
||||
|
||||
case "info":
|
||||
@@ -258,11 +269,11 @@ namespace QuantConnect.Brokerages.Bitfinex
|
||||
|
||||
case "error":
|
||||
var error = token.ToObject<ErrorMessage>();
|
||||
Log.Error($"BitfinexWebsocketsBrokerage.OnMessage: {error.Level}: {error.Message}");
|
||||
Log.Error($"BitfinexBrokerage.OnMessage: {error.Level}: {error.Message}");
|
||||
return;
|
||||
|
||||
default:
|
||||
Log.Trace($"BitfinexWebsocketsBrokerage.OnMessage: Unexpected message format: {e.Message}");
|
||||
Log.Error($"BitfinexBrokerage.OnMessage: Unexpected message format: {e.Message}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -479,7 +490,7 @@ namespace QuantConnect.Brokerages.Bitfinex
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should be empty. <see cref="BitfinexSubscriptionManager"/> manages each <see cref="BitfinexWebSocketWrapper"/> individually
|
||||
/// Should be empty. <see cref="BrokerageMultiWebSocketSubscriptionManager"/> manages each <see cref="BitfinexWebSocketWrapper"/> individually
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override IEnumerable<Symbol> GetSubscribed() => new List<Symbol>();
|
||||
|
||||
@@ -488,6 +488,9 @@ namespace QuantConnect.Brokerages.Bitfinex
|
||||
{
|
||||
_aggregator.Dispose();
|
||||
_restRateLimiter.Dispose();
|
||||
_connectionRateLimiter.Dispose();
|
||||
_onSubscribeEvent.Dispose();
|
||||
_onUnsubscribeEvent.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,753 +0,0 @@
|
||||
/*
|
||||
* 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 Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using QuantConnect.Data;
|
||||
using QuantConnect.Data.Market;
|
||||
using QuantConnect.Logging;
|
||||
using QuantConnect.Util;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using QuantConnect.Brokerages.Bitfinex.Messages;
|
||||
|
||||
namespace QuantConnect.Brokerages.Bitfinex
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles Bitfinex data subscriptions with multiple websocket connections
|
||||
/// </summary>
|
||||
public class BitfinexSubscriptionManager : DataQueueHandlerSubscriptionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum number of subscribed channels per websocket connection
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Source: https://medium.com/bitfinex/bitfinex-api-update-june-2019-661e806e6567
|
||||
/// </remarks>
|
||||
private const int MaximumSubscriptionsPerSocket = 30;
|
||||
|
||||
private const int ConnectionTimeout = 30000;
|
||||
|
||||
private readonly string _wssUrl;
|
||||
private volatile int _subscribeErrorCode;
|
||||
private readonly object _locker = new object();
|
||||
private readonly BitfinexBrokerage _brokerage;
|
||||
private readonly ISymbolMapper _symbolMapper;
|
||||
private readonly RateGate _connectionRateLimiter = new RateGate(5, TimeSpan.FromMinutes(1));
|
||||
private readonly ConcurrentDictionary<Symbol, List<BitfinexWebSocketWrapper>> _subscriptionsBySymbol = new ConcurrentDictionary<Symbol, List<BitfinexWebSocketWrapper>>();
|
||||
private readonly ConcurrentDictionary<BitfinexWebSocketWrapper, BitfinexWebSocketChannels> _channelsByWebSocket = new ConcurrentDictionary<BitfinexWebSocketWrapper, BitfinexWebSocketChannels>();
|
||||
private readonly ConcurrentDictionary<Symbol, DefaultOrderBook> _orderBooks = new ConcurrentDictionary<Symbol, DefaultOrderBook>();
|
||||
private readonly IReadOnlyDictionary<TickType, string> _tickType2ChannelName = new Dictionary<TickType, string>
|
||||
{
|
||||
{ TickType.Trade, "trades"},
|
||||
{ TickType.Quote, "book"}
|
||||
};
|
||||
private readonly ManualResetEvent _onSubscribeEvent = new ManualResetEvent(false);
|
||||
private readonly ManualResetEvent _onUnsubscribeEvent = new ManualResetEvent(false);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BitfinexSubscriptionManager"/> class.
|
||||
/// </summary>
|
||||
public BitfinexSubscriptionManager(BitfinexBrokerage brokerage, string wssUrl, ISymbolMapper symbolMapper)
|
||||
{
|
||||
_brokerage = brokerage;
|
||||
_wssUrl = wssUrl;
|
||||
_symbolMapper = symbolMapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the requested subscription (using an individual streaming channel)
|
||||
/// </summary>
|
||||
/// <param name="symbols">symbol list</param>
|
||||
/// <param name="tickType">Type of tick data</param>
|
||||
protected override bool Subscribe(IEnumerable<Symbol> symbols, TickType tickType)
|
||||
{
|
||||
try
|
||||
{
|
||||
var states = new List<bool>(symbols.Count());
|
||||
foreach (var symbol in symbols)
|
||||
{
|
||||
_onSubscribeEvent.Reset();
|
||||
_subscribeErrorCode = 0;
|
||||
var subscription = SubscribeChannel(
|
||||
ChannelNameFromTickType(tickType),
|
||||
symbol);
|
||||
|
||||
_subscriptionsBySymbol.AddOrUpdate(
|
||||
symbol,
|
||||
new List<BitfinexWebSocketWrapper> { subscription },
|
||||
(k, v) =>
|
||||
{
|
||||
if (!v.Contains(subscription))
|
||||
{
|
||||
v.Add(subscription);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
|
||||
Log.Trace($"BitfinexBrokerage.Subscribe(): Sent subscribe for {symbol.Value}/{tickType}.");
|
||||
|
||||
if (_onSubscribeEvent.WaitOne(TimeSpan.FromSeconds(10)) && _subscribeErrorCode == 0)
|
||||
{
|
||||
states.Add(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Trace($"BitfinexBrokerage.Subscribe(): Could not subscribe to {symbol.Value}/{tickType}.");
|
||||
states.Add(false);
|
||||
}
|
||||
}
|
||||
|
||||
return states.All(s => s);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error(exception);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the subscription for the requested symbol
|
||||
/// </summary>
|
||||
/// <param name="symbols">symbol list</param>
|
||||
/// <param name="tickType">Type of tick data</param>
|
||||
protected override bool Unsubscribe(IEnumerable<Symbol> symbols, TickType tickType)
|
||||
{
|
||||
var channelName = ChannelNameFromTickType(tickType);
|
||||
var states = new List<bool>(symbols.Count());
|
||||
foreach (var symbol in symbols)
|
||||
{
|
||||
List<BitfinexWebSocketWrapper> subscriptions;
|
||||
if (_subscriptionsBySymbol.TryGetValue(symbol, out subscriptions))
|
||||
{
|
||||
for (var i = subscriptions.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var webSocket = subscriptions[i];
|
||||
_onUnsubscribeEvent.Reset();
|
||||
try
|
||||
{
|
||||
var channel = new Channel(channelName, symbol);
|
||||
BitfinexWebSocketChannels channels;
|
||||
if (_channelsByWebSocket.TryGetValue(webSocket, out channels) && channels.Contains(channel))
|
||||
{
|
||||
UnsubscribeChannel(webSocket, channels, channel);
|
||||
|
||||
if (_onUnsubscribeEvent.WaitOne(TimeSpan.FromSeconds(30)))
|
||||
{
|
||||
states.Add(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Trace($"BitfinexBrokerage.Unsubscribe(): Could not unsubscribe from {symbol.Value}.");
|
||||
states.Add(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return states.All(s => s);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get channel name for a given <see cref="TickType"/>
|
||||
/// </summary>
|
||||
/// <param name="tickType"></param>
|
||||
/// <returns>Channel name <see cref="string"/></returns>
|
||||
protected override string ChannelNameFromTickType(TickType tickType)
|
||||
{
|
||||
string channelName;
|
||||
if (_tickType2ChannelName.TryGetValue(tickType, out channelName))
|
||||
{
|
||||
return channelName;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException("TickType", $"BitfinexSubscriptionManager.Subscribe(): Tick type {tickType} is not allowed for this brokerage.");
|
||||
}
|
||||
}
|
||||
|
||||
private BitfinexWebSocketWrapper SubscribeChannel(string channelName, Symbol symbol)
|
||||
{
|
||||
var channel = new Channel(channelName, symbol);
|
||||
|
||||
var webSocket = GetFreeWebSocket(channel);
|
||||
|
||||
webSocket.Send(JsonConvert.SerializeObject(new
|
||||
{
|
||||
@event = "subscribe",
|
||||
channel = channelName,
|
||||
pair = _symbolMapper.GetBrokerageSymbol(symbol)
|
||||
}));
|
||||
|
||||
return webSocket;
|
||||
}
|
||||
|
||||
private void UnsubscribeChannel(IWebSocket webSocket, BitfinexWebSocketChannels channels, Channel channel)
|
||||
{
|
||||
var channelId = channels.GetChannelId(channel);
|
||||
|
||||
webSocket.Send(JsonConvert.SerializeObject(new
|
||||
{
|
||||
@event = "unsubscribe",
|
||||
chanId = channelId.ToStringInvariant()
|
||||
}));
|
||||
}
|
||||
|
||||
private BitfinexWebSocketWrapper GetFreeWebSocket(Channel channel)
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
foreach (var kvp in _channelsByWebSocket)
|
||||
{
|
||||
if (kvp.Value.Count < MaximumSubscriptionsPerSocket)
|
||||
{
|
||||
return kvp.Key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_connectionRateLimiter.WaitToProceed(TimeSpan.Zero))
|
||||
{
|
||||
_connectionRateLimiter.WaitToProceed();
|
||||
}
|
||||
|
||||
var webSocket = new BitfinexWebSocketWrapper(
|
||||
new DefaultConnectionHandler
|
||||
{
|
||||
MaximumIdleTimeSpan = TimeSpan.FromSeconds(15)
|
||||
});
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
_channelsByWebSocket.TryAdd(webSocket, new BitfinexWebSocketChannels());
|
||||
}
|
||||
|
||||
webSocket.Initialize(_wssUrl);
|
||||
webSocket.Message += OnMessage;
|
||||
|
||||
Connect(webSocket);
|
||||
|
||||
webSocket.ConnectionHandler.ReconnectRequested += OnReconnectRequested;
|
||||
webSocket.ConnectionHandler.Initialize(webSocket.ConnectionId);
|
||||
|
||||
int connections;
|
||||
lock (_locker)
|
||||
{
|
||||
connections = _channelsByWebSocket.Count;
|
||||
}
|
||||
|
||||
Log.Trace("BitfinexSubscriptionManager.GetFreeWebSocket(): New websocket added: " +
|
||||
$"Hashcode: {webSocket.GetHashCode()}, " +
|
||||
$"WebSocket connections: {connections}");
|
||||
|
||||
return webSocket;
|
||||
}
|
||||
|
||||
private void Connect(IWebSocket webSocket)
|
||||
{
|
||||
var connectedEvent = new ManualResetEvent(false);
|
||||
EventHandler onOpenAction = (s, e) =>
|
||||
{
|
||||
connectedEvent.Set();
|
||||
};
|
||||
|
||||
webSocket.Open += onOpenAction;
|
||||
|
||||
try
|
||||
{
|
||||
webSocket.Connect();
|
||||
|
||||
if (!connectedEvent.WaitOne(ConnectionTimeout))
|
||||
{
|
||||
throw new Exception("BitfinexSubscriptionManager.Connect(): WebSocket connection timeout.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
webSocket.Open -= onOpenAction;
|
||||
|
||||
connectedEvent.DisposeSafely();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnReconnectRequested(object sender, EventArgs e)
|
||||
{
|
||||
var connectionHandler = (DefaultConnectionHandler)sender;
|
||||
|
||||
Log.Trace($"BitfinexSubscriptionManager.OnReconnectRequested(): WebSocket reconnection requested [Id: {connectionHandler.ConnectionId}]");
|
||||
|
||||
BitfinexWebSocketWrapper webSocket = null;
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
webSocket = _channelsByWebSocket.Keys
|
||||
.FirstOrDefault(connection => connection.ConnectionId == connectionHandler.ConnectionId);
|
||||
}
|
||||
|
||||
if (webSocket == null)
|
||||
{
|
||||
Log.Error($"BitfinexSubscriptionManager.OnReconnectRequested(): WebSocket ConnectionId not found: {connectionHandler.ConnectionId}");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Trace($"BitfinexSubscriptionManager.OnReconnectRequested(): IsOpen:{webSocket.IsOpen} [Id: {connectionHandler.ConnectionId}]");
|
||||
|
||||
if (!webSocket.IsOpen)
|
||||
{
|
||||
Log.Trace($"BitfinexSubscriptionManager.OnReconnectRequested(): Websocket connecting. [Id: {connectionHandler.ConnectionId}]");
|
||||
webSocket.Connect();
|
||||
}
|
||||
|
||||
if (!webSocket.IsOpen)
|
||||
{
|
||||
Log.Trace($"BitfinexSubscriptionManager.OnReconnectRequested(): Websocket not open: IsOpen:{webSocket.IsOpen} [Id: {connectionHandler.ConnectionId}]");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Trace($"BitfinexSubscriptionManager.OnReconnectRequested(): Reconnected: IsOpen:{webSocket.IsOpen} [Id: {connectionHandler.ConnectionId}]");
|
||||
|
||||
BitfinexWebSocketChannels channels;
|
||||
lock (_locker)
|
||||
{
|
||||
if (!_channelsByWebSocket.TryGetValue(webSocket, out channels))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Trace($"BitfinexSubscriptionManager.OnReconnectRequested(): Resubscribing channels. [Id: {connectionHandler.ConnectionId}]");
|
||||
|
||||
foreach (var channel in channels.Values)
|
||||
{
|
||||
webSocket.Send(JsonConvert.SerializeObject(new
|
||||
{
|
||||
@event = "subscribe",
|
||||
channel = channel.Name,
|
||||
pair = _symbolMapper.GetBrokerageSymbol(channel.Symbol)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMessage(object sender, WebSocketMessage e)
|
||||
{
|
||||
var webSocket = (BitfinexWebSocketWrapper)sender;
|
||||
|
||||
try
|
||||
{
|
||||
var token = JToken.Parse(e.Message);
|
||||
|
||||
webSocket.ConnectionHandler.KeepAlive(DateTime.UtcNow);
|
||||
|
||||
if (token is JArray)
|
||||
{
|
||||
var channel = token[0].ToObject<int>();
|
||||
|
||||
if (token[1].Type == JTokenType.String)
|
||||
{
|
||||
var type = token[1].Value<string>();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
// heartbeat
|
||||
case "hb":
|
||||
return;
|
||||
|
||||
// trade execution
|
||||
case "te":
|
||||
OnUpdate(webSocket, channel, token[2].ToObject<string[]>());
|
||||
break;
|
||||
|
||||
// ignored -- trades already handled in "te" message
|
||||
// https://github.com/bitfinexcom/bitfinex-api-node#te-vs-tu-messages
|
||||
case "tu":
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.Trace($"BitfinexSubscriptionManager.OnMessage(): Unexpected message type: {type}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// public channels
|
||||
else if (channel != 0 && token[1].Type == JTokenType.Array)
|
||||
{
|
||||
if (token[1][0].Type == JTokenType.Array)
|
||||
{
|
||||
OnSnapshot(
|
||||
webSocket,
|
||||
channel,
|
||||
token[1].ToObject<string[][]>()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// pass channel id as separate arg
|
||||
OnUpdate(
|
||||
webSocket,
|
||||
channel,
|
||||
token[1].ToObject<string[]>()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (token is JObject)
|
||||
{
|
||||
var raw = token.ToObject<BaseMessage>();
|
||||
switch (raw.Event.ToLowerInvariant())
|
||||
{
|
||||
case "subscribed":
|
||||
OnSubscribe(webSocket, token.ToObject<ChannelSubscription>());
|
||||
return;
|
||||
|
||||
case "unsubscribed":
|
||||
OnUnsubscribe(webSocket, token.ToObject<ChannelUnsubscribing>());
|
||||
return;
|
||||
|
||||
case "auth":
|
||||
case "info":
|
||||
case "ping":
|
||||
return;
|
||||
|
||||
case "error":
|
||||
var error = token.ToObject<ErrorMessage>();
|
||||
// 10300 Subscription failed (generic) | 10301 : Already subscribed | 10302 : Unknown channel
|
||||
// see https://docs.bitfinex.com/docs/ws-general
|
||||
if (error.Code == 10300 || error.Code == 10301 || error.Code == 10302)
|
||||
{
|
||||
_subscribeErrorCode = error.Code;
|
||||
_onSubscribeEvent.Set();
|
||||
}
|
||||
Log.Error($"BitfinexSubscriptionManager.OnMessage(): {e.Message}");
|
||||
return;
|
||||
|
||||
default:
|
||||
Log.Trace($"BitfinexSubscriptionManager.OnMessage(): Unexpected message format: {e.Message}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_brokerage.OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Error, -1, $"Parsing wss message failed. Data: {e.Message} Exception: {exception}"));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSubscribe(BitfinexWebSocketWrapper webSocket, ChannelSubscription data)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
var channel = new Channel(data.Channel, _symbolMapper.GetLeanSymbol(data.Symbol, SecurityType.Crypto, Market.Bitfinex));
|
||||
|
||||
BitfinexWebSocketChannels channels;
|
||||
if (!_channelsByWebSocket.TryGetValue(webSocket, out channels))
|
||||
{
|
||||
_onSubscribeEvent.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
// we need to update the channel on re subscription
|
||||
channels.AddOrUpdate(data.ChannelId, channel);
|
||||
|
||||
Log.Trace($"BitfinexSubscriptionManager.OnSubscribe(): Channel subscribed: Id:{data.ChannelId} {channel.Symbol}/{channel.Name}");
|
||||
|
||||
_onSubscribeEvent.Set();
|
||||
|
||||
webSocket.ConnectionHandler.EnableMonitoring(true);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUnsubscribe(BitfinexWebSocketWrapper webSocket, ChannelUnsubscribing data)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
BitfinexWebSocketChannels channels;
|
||||
if (!_channelsByWebSocket.TryGetValue(webSocket, out channels))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Channel channel;
|
||||
if (!channels.TryRemove(data.ChannelId, out channel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_onUnsubscribeEvent.Set();
|
||||
|
||||
if (channels.Values.Count(c => c.Symbol.Equals(channel.Symbol)) == 0)
|
||||
{
|
||||
List<BitfinexWebSocketWrapper> subscriptions;
|
||||
if (_subscriptionsBySymbol.TryGetValue(channel.Symbol, out subscriptions))
|
||||
{
|
||||
subscriptions.Remove(webSocket);
|
||||
|
||||
if (subscriptions.Count == 0)
|
||||
{
|
||||
_subscriptionsBySymbol.TryRemove(channel.Symbol, out subscriptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (channels.Count != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_channelsByWebSocket.TryRemove(webSocket, out channels);
|
||||
}
|
||||
|
||||
webSocket.Close();
|
||||
webSocket.ConnectionHandler.DisposeSafely();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSnapshot(BitfinexWebSocketWrapper webSocket, int channelId, string[][] entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
Channel channel;
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
BitfinexWebSocketChannels channels;
|
||||
if (!_channelsByWebSocket.TryGetValue(webSocket, out channels))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channels.TryGetValue(channelId, out channel))
|
||||
{
|
||||
_brokerage.OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, -1, $"Message received from unknown channel Id {channelId}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (channel.Name.ToLowerInvariant())
|
||||
{
|
||||
case "book":
|
||||
ProcessOrderBookSnapshot(channel, entries);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessOrderBookSnapshot(Channel channel, string[][] entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
var symbol = channel.Symbol;
|
||||
|
||||
DefaultOrderBook orderBook;
|
||||
if (!_orderBooks.TryGetValue(symbol, out orderBook))
|
||||
{
|
||||
orderBook = new DefaultOrderBook(symbol);
|
||||
_orderBooks[symbol] = orderBook;
|
||||
}
|
||||
else
|
||||
{
|
||||
orderBook.BestBidAskUpdated -= OnBestBidAskUpdated;
|
||||
orderBook.Clear();
|
||||
}
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var price = decimal.Parse(entry[0], NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
var amount = decimal.Parse(entry[2], NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
|
||||
if (amount > 0)
|
||||
orderBook.UpdateBidRow(price, amount);
|
||||
else
|
||||
orderBook.UpdateAskRow(price, Math.Abs(amount));
|
||||
}
|
||||
|
||||
orderBook.BestBidAskUpdated += OnBestBidAskUpdated;
|
||||
|
||||
EmitQuoteTick(symbol, orderBook.BestBidPrice, orderBook.BestBidSize, orderBook.BestAskPrice, orderBook.BestAskSize);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUpdate(BitfinexWebSocketWrapper webSocket, int channelId, string[] entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
Channel channel;
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
BitfinexWebSocketChannels channels;
|
||||
if (!_channelsByWebSocket.TryGetValue(webSocket, out channels))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channels.TryGetValue(channelId, out channel))
|
||||
{
|
||||
_brokerage.OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, -1, $"Message received from unknown channel Id {channelId}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (channel.Name.ToLowerInvariant())
|
||||
{
|
||||
case "book":
|
||||
ProcessOrderBookUpdate(channel, entries);
|
||||
return;
|
||||
|
||||
case "trades":
|
||||
ProcessTradeUpdate(channel, entries);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessOrderBookUpdate(Channel channel, string[] entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
var symbol = channel.Symbol;
|
||||
var orderBook = _orderBooks[symbol];
|
||||
|
||||
var price = decimal.Parse(entries[0], NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
var count = Parse.Long(entries[1]);
|
||||
var amount = decimal.Parse(entries[2], NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
orderBook.RemovePriceLevel(price);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (amount > 0)
|
||||
{
|
||||
orderBook.UpdateBidRow(price, amount);
|
||||
}
|
||||
else if (amount < 0)
|
||||
{
|
||||
orderBook.UpdateAskRow(price, Math.Abs(amount));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Entries: [{string.Join(",", entries)}]");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessTradeUpdate(Channel channel, string[] entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
var time = Time.UnixMillisecondTimeStampToDateTime(decimal.Parse(entries[1], NumberStyles.Float, CultureInfo.InvariantCulture));
|
||||
var amount = decimal.Parse(entries[2], NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
var price = decimal.Parse(entries[3], NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
|
||||
EmitTradeTick(channel.Symbol, time, price, amount);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitTradeTick(Symbol symbol, DateTime time, decimal price, decimal amount)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_brokerage.TickLocker)
|
||||
{
|
||||
_brokerage.EmitTick(new Tick
|
||||
{
|
||||
Value = price,
|
||||
Time = time,
|
||||
Symbol = symbol,
|
||||
TickType = TickType.Trade,
|
||||
Quantity = Math.Abs(amount)
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitQuoteTick(Symbol symbol, decimal bidPrice, decimal bidSize, decimal askPrice, decimal askSize)
|
||||
{
|
||||
lock (_brokerage.TickLocker)
|
||||
{
|
||||
_brokerage.EmitTick(new Tick
|
||||
{
|
||||
AskPrice = askPrice,
|
||||
BidPrice = bidPrice,
|
||||
Value = (askPrice + bidPrice) / 2m,
|
||||
Time = DateTime.UtcNow,
|
||||
Symbol = symbol,
|
||||
TickType = TickType.Quote,
|
||||
AskSize = Math.Abs(askSize),
|
||||
BidSize = Math.Abs(bidSize)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBestBidAskUpdated(object sender, BestBidAskUpdatedEventArgs e)
|
||||
{
|
||||
EmitQuoteTick(e.Symbol, e.BestBidPrice, e.BestBidSize, e.BestAskPrice, e.BestAskSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
138
Brokerages/BrokerageMultiWebSocketEntry.cs
Normal file
138
Brokerages/BrokerageMultiWebSocketEntry.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace QuantConnect.Brokerages
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for <see cref="BrokerageMultiWebSocketSubscriptionManager"/>
|
||||
/// </summary>
|
||||
public class BrokerageMultiWebSocketEntry
|
||||
{
|
||||
private readonly Dictionary<Symbol, int> _symbolWeights;
|
||||
private readonly List<Symbol> _symbols;
|
||||
private readonly object _locker = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the web socket instance
|
||||
/// </summary>
|
||||
public IWebSocket WebSocket { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sum of symbol weights for this web socket
|
||||
/// </summary>
|
||||
public int TotalWeight { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of symbols subscribed
|
||||
/// </summary>
|
||||
public int SymbolCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
return _symbols.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the symbol is subscribed
|
||||
/// </summary>
|
||||
/// <param name="symbol"></param>
|
||||
/// <returns></returns>
|
||||
public bool Contains(Symbol symbol)
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
return _symbols.Contains(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the list of subscribed symbols
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IReadOnlyCollection<Symbol> Symbols
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
return _symbols.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BrokerageMultiWebSocketEntry"/> class
|
||||
/// </summary>
|
||||
/// <param name="symbolWeights">A dictionary of symbol weights</param>
|
||||
/// <param name="webSocket">The web socket instance</param>
|
||||
public BrokerageMultiWebSocketEntry(Dictionary<Symbol, int> symbolWeights, IWebSocket webSocket)
|
||||
{
|
||||
_symbolWeights = symbolWeights;
|
||||
_symbols = new List<Symbol>();
|
||||
|
||||
WebSocket = webSocket;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BrokerageMultiWebSocketEntry"/> class
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The web socket instance</param>
|
||||
public BrokerageMultiWebSocketEntry(IWebSocket webSocket)
|
||||
: this(null, webSocket)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a symbol to the entry
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to add</param>
|
||||
public void AddSymbol(Symbol symbol)
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
_symbols.Add(symbol);
|
||||
}
|
||||
|
||||
if (_symbolWeights != null && _symbolWeights.TryGetValue(symbol, out var weight))
|
||||
{
|
||||
TotalWeight += weight;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a symbol from the entry
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to remove</param>
|
||||
public void RemoveSymbol(Symbol symbol)
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
_symbols.Remove(symbol);
|
||||
}
|
||||
|
||||
if (_symbolWeights != null && _symbolWeights.TryGetValue(symbol, out var weight))
|
||||
{
|
||||
TotalWeight -= weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
255
Brokerages/BrokerageMultiWebSocketSubscriptionManager.cs
Normal file
255
Brokerages/BrokerageMultiWebSocketSubscriptionManager.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* 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;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using QuantConnect.Data;
|
||||
using QuantConnect.Logging;
|
||||
using QuantConnect.Util;
|
||||
|
||||
namespace QuantConnect.Brokerages
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles brokerage data subscriptions with multiple websocket connections, with optional symbol weighting
|
||||
/// </summary>
|
||||
public class BrokerageMultiWebSocketSubscriptionManager : EventBasedDataQueueHandlerSubscriptionManager
|
||||
{
|
||||
private readonly string _webSocketUrl;
|
||||
private readonly int _maximumSymbolsPerWebSocket;
|
||||
private readonly int _maximumWebSocketConnections;
|
||||
private readonly Func<WebSocketClientWrapper> _webSocketFactory;
|
||||
private readonly Func<IWebSocket, Symbol, bool> _subscribeFunc;
|
||||
private readonly Func<IWebSocket, Symbol, bool> _unsubscribeFunc;
|
||||
private readonly Action<WebSocketMessage> _messageHandler;
|
||||
private readonly RateGate _connectionRateLimiter;
|
||||
|
||||
private const int ConnectionTimeout = 30000;
|
||||
|
||||
private readonly object _locker = new();
|
||||
private readonly List<BrokerageMultiWebSocketEntry> _webSocketEntries = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BrokerageMultiWebSocketSubscriptionManager"/> class
|
||||
/// </summary>
|
||||
/// <param name="webSocketUrl">The URL for websocket connections</param>
|
||||
/// <param name="maximumSymbolsPerWebSocket">The maximum number of symbols per websocket connection</param>
|
||||
/// <param name="maximumWebSocketConnections">The maximum number of websocket connections allowed (if zero, symbol weighting is disabled)</param>
|
||||
/// <param name="connectionRateLimiter">The rate limiter for creating new websocket connections</param>
|
||||
/// <param name="symbolWeights">A dictionary for the symbol weights</param>
|
||||
/// <param name="webSocketFactory">A function which returns a new websocket instance</param>
|
||||
/// <param name="subscribeFunc">A function which subscribes a symbol</param>
|
||||
/// <param name="unsubscribeFunc">A function which unsubscribes a symbol</param>
|
||||
/// <param name="messageHandler">The websocket message handler</param>
|
||||
public BrokerageMultiWebSocketSubscriptionManager(
|
||||
string webSocketUrl,
|
||||
int maximumSymbolsPerWebSocket,
|
||||
int maximumWebSocketConnections,
|
||||
Dictionary<Symbol, int> symbolWeights,
|
||||
Func<WebSocketClientWrapper> webSocketFactory,
|
||||
Func<IWebSocket, Symbol, bool> subscribeFunc,
|
||||
Func<IWebSocket, Symbol, bool> unsubscribeFunc,
|
||||
Action<WebSocketMessage> messageHandler,
|
||||
RateGate connectionRateLimiter = null)
|
||||
{
|
||||
_webSocketUrl = webSocketUrl;
|
||||
_maximumSymbolsPerWebSocket = maximumSymbolsPerWebSocket;
|
||||
_maximumWebSocketConnections = maximumWebSocketConnections;
|
||||
_webSocketFactory = webSocketFactory;
|
||||
_subscribeFunc = subscribeFunc;
|
||||
_unsubscribeFunc = unsubscribeFunc;
|
||||
_messageHandler = messageHandler;
|
||||
_connectionRateLimiter = connectionRateLimiter;
|
||||
|
||||
if (_maximumWebSocketConnections > 0)
|
||||
{
|
||||
// symbol weighting enabled, create all websocket instances
|
||||
for (var i = 0; i < _maximumWebSocketConnections; i++)
|
||||
{
|
||||
var webSocket = _webSocketFactory();
|
||||
webSocket.Open += OnOpen;
|
||||
|
||||
_webSocketEntries.Add(new BrokerageMultiWebSocketEntry(symbolWeights, webSocket));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the symbols
|
||||
/// </summary>
|
||||
/// <param name="symbols">Symbols to subscribe</param>
|
||||
/// <param name="tickType">Type of tick data</param>
|
||||
protected override bool Subscribe(IEnumerable<Symbol> symbols, TickType tickType)
|
||||
{
|
||||
Log.Trace($"BrokerageMultiWebSocketSubscriptionManager.Subscribe(): {string.Join(",", symbols.Select(x => x.Value))}");
|
||||
|
||||
var success = true;
|
||||
|
||||
foreach (var symbol in symbols)
|
||||
{
|
||||
var webSocket = GetWebSocketForSymbol(symbol);
|
||||
|
||||
success &= _subscribeFunc(webSocket, symbol);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from the symbols
|
||||
/// </summary>
|
||||
/// <param name="symbols">Symbols to subscribe</param>
|
||||
/// <param name="tickType">Type of tick data</param>
|
||||
protected override bool Unsubscribe(IEnumerable<Symbol> symbols, TickType tickType)
|
||||
{
|
||||
Log.Trace($"BrokerageMultiWebSocketSubscriptionManager.Unsubscribe(): {string.Join(",", symbols.Select(x => x.Value))}");
|
||||
|
||||
var success = true;
|
||||
|
||||
foreach (var symbol in symbols)
|
||||
{
|
||||
var entry = GetWebSocketEntryBySymbol(symbol);
|
||||
if (entry != null)
|
||||
{
|
||||
entry.RemoveSymbol(symbol);
|
||||
|
||||
success &= _unsubscribeFunc(entry.WebSocket, symbol);
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private BrokerageMultiWebSocketEntry GetWebSocketEntryBySymbol(Symbol symbol)
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
foreach (var entry in _webSocketEntries.Where(entry => entry.Contains(symbol)))
|
||||
{
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a symbol to an existing or new websocket connection
|
||||
/// </summary>
|
||||
private IWebSocket GetWebSocketForSymbol(Symbol symbol)
|
||||
{
|
||||
BrokerageMultiWebSocketEntry entry;
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
if (_webSocketEntries.All(x => x.SymbolCount >= _maximumSymbolsPerWebSocket))
|
||||
{
|
||||
if (_maximumWebSocketConnections > 0)
|
||||
{
|
||||
throw new NotSupportedException($"Maximum symbol count reached for the current configuration [MaxSymbolsPerWebSocket={_maximumSymbolsPerWebSocket}, MaxWebSocketConnections:{_maximumWebSocketConnections}]");
|
||||
}
|
||||
|
||||
// symbol limit reached on all, create new websocket instance
|
||||
var webSocket = _webSocketFactory();
|
||||
webSocket.Open += OnOpen;
|
||||
|
||||
_webSocketEntries.Add(new BrokerageMultiWebSocketEntry(webSocket));
|
||||
}
|
||||
|
||||
// sort by weight ascending, taking into account the symbol limit per websocket
|
||||
_webSocketEntries.Sort((x, y) =>
|
||||
x.SymbolCount >= _maximumSymbolsPerWebSocket
|
||||
? 1
|
||||
: y.SymbolCount >= _maximumSymbolsPerWebSocket
|
||||
? -1
|
||||
: Math.Sign(x.TotalWeight - y.TotalWeight));
|
||||
|
||||
entry = _webSocketEntries.First();
|
||||
}
|
||||
|
||||
if (!entry.WebSocket.IsOpen)
|
||||
{
|
||||
Connect(entry.WebSocket);
|
||||
}
|
||||
|
||||
entry.AddSymbol(symbol);
|
||||
|
||||
Log.Trace($"BrokerageMultiWebSocketSubscriptionManager.GetWebSocketForSymbol(): added symbol: {symbol} to websocket: {entry.WebSocket.GetHashCode()} - Count: {entry.SymbolCount}");
|
||||
|
||||
return entry.WebSocket;
|
||||
}
|
||||
|
||||
private void Connect(IWebSocket webSocket)
|
||||
{
|
||||
webSocket.Initialize(_webSocketUrl);
|
||||
webSocket.Message += (s, e) => _messageHandler(e);
|
||||
|
||||
var connectedEvent = new ManualResetEvent(false);
|
||||
EventHandler onOpenAction = (_, _) =>
|
||||
{
|
||||
connectedEvent.Set();
|
||||
};
|
||||
|
||||
webSocket.Open += onOpenAction;
|
||||
|
||||
if (_connectionRateLimiter is { IsRateLimited: false })
|
||||
{
|
||||
_connectionRateLimiter.WaitToProceed();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
webSocket.Connect();
|
||||
|
||||
if (!connectedEvent.WaitOne(ConnectionTimeout))
|
||||
{
|
||||
throw new TimeoutException($"BrokerageMultiWebSocketSubscriptionManager.Connect(): WebSocket connection timeout: {webSocket.GetHashCode()}");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
webSocket.Open -= onOpenAction;
|
||||
|
||||
connectedEvent.DisposeSafely();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpen(object sender, EventArgs e)
|
||||
{
|
||||
var webSocket = (IWebSocket)sender;
|
||||
|
||||
lock (_locker)
|
||||
{
|
||||
foreach (var entry in _webSocketEntries)
|
||||
{
|
||||
if (entry.WebSocket == webSocket && entry.Symbols.Count > 0)
|
||||
{
|
||||
Log.Trace($"BrokerageMultiWebSocketSubscriptionManager.Connect(): WebSocket opened: {webSocket.GetHashCode()} - Resubscribing existing symbols: {entry.Symbols.Count}");
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
foreach (var symbol in entry.Symbols)
|
||||
{
|
||||
_subscribeFunc(webSocket, symbol);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
*
|
||||
@@ -70,6 +70,8 @@ namespace QuantConnect.Brokerages
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
_client = null;
|
||||
|
||||
_taskConnect = Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
@@ -93,7 +95,18 @@ namespace QuantConnect.Brokerages
|
||||
|
||||
Log.Trace($"WebSocketClientWrapper connection task ended: {_url}");
|
||||
},
|
||||
_cts.Token);
|
||||
_cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
|
||||
var count = 0;
|
||||
do
|
||||
{
|
||||
// wait for _client to be not null
|
||||
if (_client != null || _cts.Token.WaitHandle.WaitOne(50))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (++count < 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,24 +116,30 @@ namespace QuantConnect.Brokerages
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
try
|
||||
lock (_locker)
|
||||
{
|
||||
_client?.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", _cts.Token).SynchronouslyAwaitTask();
|
||||
try
|
||||
{
|
||||
_client?.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", _cts.Token).SynchronouslyAwaitTask();
|
||||
|
||||
_cts?.Cancel();
|
||||
_cts?.Cancel();
|
||||
|
||||
_taskConnect?.Wait(TimeSpan.FromSeconds(5));
|
||||
_taskConnect?.Wait(TimeSpan.FromSeconds(5));
|
||||
|
||||
_cts.DisposeSafely();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"WebSocketClientWrapper.Close({_url}): {e}");
|
||||
_cts.DisposeSafely();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"WebSocketClientWrapper.Close({_url}): {e}");
|
||||
}
|
||||
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
_cts = null;
|
||||
|
||||
OnClose(new WebSocketCloseData(0, string.Empty, true));
|
||||
if (_client != null)
|
||||
{
|
||||
OnClose(new WebSocketCloseData(0, string.Empty, true));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -196,22 +215,35 @@ namespace QuantConnect.Brokerages
|
||||
await _client.ConnectAsync(new Uri(_url), connectionCts.Token);
|
||||
OnOpen();
|
||||
|
||||
var receiveBuffer = new byte[ReceiveBufferSize];
|
||||
|
||||
while ((_client.State == WebSocketState.Open || _client.State == WebSocketState.CloseSent) &&
|
||||
!connectionCts.IsCancellationRequested)
|
||||
{
|
||||
var messageData = await ReceiveMessage(_client, connectionCts.Token);
|
||||
var messageData = await ReceiveMessage(_client, connectionCts.Token, receiveBuffer);
|
||||
|
||||
if (messageData.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
Log.Trace($"WebSocketClientWrapper.HandleConnection({_url}): WebSocketMessageType.Close");
|
||||
var data = string.Empty;
|
||||
if (messageData.Data != null)
|
||||
{
|
||||
data = Encoding.UTF8.GetString(messageData.Data);
|
||||
}
|
||||
|
||||
Log.Trace($"WebSocketClientWrapper.HandleConnection({_url}): WebSocketMessageType.Close - Data: {data}");
|
||||
return;
|
||||
}
|
||||
|
||||
var message = Encoding.UTF8.GetString(messageData.Data);
|
||||
OnMessage(new WebSocketMessage(message));
|
||||
OnMessage(new WebSocketMessage(this, message));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
OnError(new WebSocketError(ex.Message, ex));
|
||||
connectionCts.Token.WaitHandle.WaitOne(2000);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnError(new WebSocketError(ex.Message, ex));
|
||||
@@ -222,9 +254,10 @@ namespace QuantConnect.Brokerages
|
||||
private async Task<MessageData> ReceiveMessage(
|
||||
WebSocket webSocket,
|
||||
CancellationToken ct,
|
||||
byte[] receiveBuffer,
|
||||
long maxSize = long.MaxValue)
|
||||
{
|
||||
var buffer = new ArraySegment<byte>(new byte[ReceiveBufferSize]);
|
||||
var buffer = new ArraySegment<byte>(receiveBuffer);
|
||||
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
|
||||
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
|
||||
*
|
||||
@@ -20,6 +20,11 @@ namespace QuantConnect.Brokerages
|
||||
/// </summary>
|
||||
public class WebSocketMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the sender websocket instance
|
||||
/// </summary>
|
||||
public IWebSocket WebSocket { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw message data as text
|
||||
/// </summary>
|
||||
@@ -28,10 +33,12 @@ namespace QuantConnect.Brokerages
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebSocketMessage"/> class
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The sender websocket instance</param>
|
||||
/// <param name="message">The message</param>
|
||||
public WebSocketMessage(string message)
|
||||
public WebSocketMessage(IWebSocket webSocket, string message)
|
||||
{
|
||||
WebSocket = webSocket;
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,13 +467,15 @@ gdax,ZRXBTC,crypto,0x-Bitcoin,BTC,1,0.00000001,0.00001,ZRX-BTC
|
||||
gdax,ZRXEUR,crypto,0x-Euro,EUR,1,0.000001,0.00001,ZRX-EUR
|
||||
gdax,ZRXUSD,crypto,0x-United States Dollar,USD,1,0.000001,0.00001,ZRX-USD
|
||||
|
||||
bitfinex,ABYSSUSD,crypto,The Abyss-US Dollar,USD,1,0.00001,0.00000001,tABSUSD
|
||||
bitfinex,1INCHUSD,crypto,1INCH-US Dollar,USD,1,0.00001,0.00000001,t1INCH:USD
|
||||
bitfinex,1INCHUSDT,crypto,1INCH-Tether USDt,USDT,1,0.00001,0.00000001,t1INCH:UST
|
||||
bitfinex,AAVEUSD,crypto,AAVE-US Dollar,USD,1,0.00001,0.00000001,tAAVE:USD
|
||||
bitfinex,AAVEUSDT,crypto,AAVE-Tether USDt,USDT,1,0.00001,0.00000001,tAAVE:UST
|
||||
bitfinex,ADABTC,crypto,Cardano ADA-Bitcoin,BTC,1,0.00001,0.00000001,tADABTC
|
||||
bitfinex,ADAUSD,crypto,Cardano ADA-US Dollar,USD,1,0.00001,0.00000001,tADAUSD
|
||||
bitfinex,ADAUSDT,crypto,Cardano ADA-Tether USDt,USDT,1,0.00001,0.00000001,tADAUST
|
||||
bitfinex,AGIUSD,crypto,SingularityNET-US Dollar,USD,1,0.00001,0.00000001,tAGIUSD
|
||||
bitfinex,AIDUSD,crypto,AidCoin-US Dollar,USD,1,0.00001,0.00000001,tAIDUSD
|
||||
bitfinex,AIONUSD,crypto,Aion-US Dollar,USD,1,0.00001,0.00000001,tAIOUSD
|
||||
bitfinex,ALBTUSD,crypto,AllianceBlock-US Dollar,USD,1,0.00001,0.00000001,tALBT:USD
|
||||
bitfinex,ALBTUSDT,crypto,AllianceBlock-Tether USDt,USDT,1,0.00001,0.00000001,tALBT:UST
|
||||
bitfinex,ALGOBTC,crypto,Algorand-Bitcoin,BTC,1,0.00001,0.00000001,tALGBTC
|
||||
bitfinex,ALGOUSD,crypto,Algorand-US Dollar,USD,1,0.00001,0.00000001,tALGUSD
|
||||
bitfinex,ALGOUSDT,crypto,Algorand-Tether USDt,USDT,1,0.00001,0.00000001,tALGUST
|
||||
@@ -483,18 +485,13 @@ bitfinex,AMPLUSDT,crypto,Ampleforth-Tether USDt,USDT,1,0.00001,0.00000001,tAMPUS
|
||||
bitfinex,ANTBTC,crypto,Aragon-Bitcoin,BTC,1,0.00001,0.00000001,tANTBTC
|
||||
bitfinex,ANTETH,crypto,Aragon-Ethereum,ETH,1,0.00001,0.00000001,tANTETH
|
||||
bitfinex,ANTUSD,crypto,Aragon-US Dollar,USD,1,0.00001,0.00000001,tANTUSD
|
||||
bitfinex,ASTUSD,crypto,AirSwap-US Dollar,USD,1,0.00001,0.00000001,tASTUSD
|
||||
bitfinex,ATMUSD,crypto,Atonomi-US Dollar,USD,1,0.00001,0.00000001,tATMUSD
|
||||
bitfinex,ATOMBTC,crypto,Cosmos-Atom-Bitcoin,BTC,1,0.00001,0.00000001,tATOBTC
|
||||
bitfinex,ATOMETH,crypto,Cosmos-Atom-Ethereum,ETH,1,0.00001,0.00000001,tATOETH
|
||||
bitfinex,ATOMUSD,crypto,Cosmos-Atom-US Dollar,USD,1,0.00001,0.00000001,tATOUSD
|
||||
bitfinex,AUCUSD,crypto,Auctus-US Dollar,USD,1,0.00001,0.00000001,tAUCUSD
|
||||
bitfinex,AVAXUSD,crypto,Avalanche-US Dollar,USD,1,0.00001,0.00000001,tAVAX:USD
|
||||
bitfinex,AVAXUSDT,crypto,Avalanche-Tether USDt,USDT,1,0.00001,0.00000001,tAVAX:UST
|
||||
bitfinex,AVTUSD,crypto,Aventus-US Dollar,USD,1,0.00001,0.00000001,tAVTUSD
|
||||
bitfinex,BCHBTC,crypto,Bitcoin Cash-Bitcoin,BTC,1,0.00001,0.00000001,tBABBTC
|
||||
bitfinex,BCHUSD,crypto,Bitcoin Cash-US Dollar,USD,1,0.00001,0.00000001,tBABUSD
|
||||
bitfinex,BCHUSDT,crypto,Bitcoin Cash-Tether USDt,USDT,1,0.00001,0.00000001,tBABUST
|
||||
bitfinex,B21USD,crypto,B21-US Dollar,USD,1,0.00001,0.00000001,tB21X:USD
|
||||
bitfinex,B21USDT,crypto,B21-Tether USDt,USDT,1,0.00001,0.00000001,tB21X:UST
|
||||
bitfinex,BALUSD,crypto,Balancer-US Dollar,USD,1,0.00001,0.00000001,tBALUSD
|
||||
bitfinex,BALUSDT,crypto,Balancer-Tether USDt,USDT,1,0.00001,0.00000001,tBALUST
|
||||
bitfinex,BANDUSD,crypto,Band-US Dollar,USD,1,0.00001,0.00000001,tBAND:USD
|
||||
@@ -502,9 +499,15 @@ bitfinex,BANDUSDT,crypto,Band-Tether USDt,USDT,1,0.00001,0.00000001,tBAND:UST
|
||||
bitfinex,BATBTC,crypto,Basic Attention Token-Bitcoin,BTC,1,0.00001,0.00000001,tBATBTC
|
||||
bitfinex,BATETH,crypto,Basic Attention Token-Ethereum,ETH,1,0.00001,0.00000001,tBATETH
|
||||
bitfinex,BATUSD,crypto,Basic Attention Token-US Dollar,USD,1,0.00001,0.00000001,tBATUSD
|
||||
bitfinex,BCHABCUSD,crypto,xec-US Dollar,USD,1,0.00001,0.00000001,tBCHABC:USD
|
||||
bitfinex,BCHNUSD,crypto,BCH Node-US Dollar,USD,1,0.00001,0.00000001,tBCHN:USD
|
||||
bitfinex,BESTUSD,crypto,Bitpanda-US Dollar,USD,1,0.00001,0.00000001,tBEST:USD
|
||||
bitfinex,BFTUSD,crypto,BnkToTheFuture-US Dollar,USD,1,0.00001,0.00000001,tBFTUSD
|
||||
bitfinex,BMIUSD,crypto,BridgeMutual-US Dollar,USD,1,0.00001,0.00000001,tBMIUSD
|
||||
bitfinex,BMIUSDT,crypto,BridgeMutual-Tether USDt,USDT,1,0.00001,0.00000001,tBMIUST
|
||||
bitfinex,BNTUSD,crypto,Bancor-US Dollar,USD,1,0.00001,0.00000001,tBNTUSD
|
||||
bitfinex,BOXUSD,crypto,ContentBox-US Dollar,USD,1,0.00001,0.00000001,tBOXUSD
|
||||
bitfinex,BOSONUSD,crypto,Boson Token-US Dollar,USD,1,0.00001,0.00000001,tBOSON:USD
|
||||
bitfinex,BOSONUSDT,crypto,Boson Token-Tether USDt,USDT,1,0.00001,0.00000001,tBOSON:UST
|
||||
bitfinex,BSVBTC,crypto,Bitcoin SV-Bitcoin,BTC,1,0.00001,0.00000001,tBSVBTC
|
||||
bitfinex,BSVUSD,crypto,Bitcoin SV-US Dollar,USD,1,0.00001,0.00000001,tBSVUSD
|
||||
bitfinex,BTCCNHT,crypto,Bitcoin-Tether CNHt,CNHT,1,0.00001,0.00000001,tBTC:CNHT
|
||||
@@ -518,37 +521,33 @@ bitfinex,BTGBTC,crypto,Bitcoin Gold-Bitcoin,BTC,1,0.00001,0.00000001,tBTGBTC
|
||||
bitfinex,BTGUSD,crypto,Bitcoin Gold-US Dollar,USD,1,0.00001,0.00000001,tBTGUSD
|
||||
bitfinex,BTSEUSD,crypto,BTSE-US Dollar,USD,1,0.00001,0.00000001,tBTSE:USD
|
||||
bitfinex,BTTUSD,crypto,BitTorrent-US Dollar,USD,1,0.00001,0.00000001,tBTTUSD
|
||||
bitfinex,CBTUSD,crypto,CommerceBlock-US Dollar,USD,1,0.00001,0.00000001,tCBTUSD
|
||||
bitfinex,CELUSD,crypto,Celsius-US Dollar,USD,1,0.00001,0.00000001,tCELUSD
|
||||
bitfinex,CELUSDT,crypto,Celsius-Tether USDt,USDT,1,0.00001,0.00000001,tCELUST
|
||||
bitfinex,CHEXUSD,crypto,CHEX-US Dollar,USD,1,0.00001,0.00000001,tCHEX:USD
|
||||
bitfinex,CHZUSD,crypto,socios.com Chiliz-US Dollar,USD,1,0.00001,0.00000001,tCHZUSD
|
||||
bitfinex,CHZUSDT,crypto,socios.com Chiliz-Tether USDt,USDT,1,0.00001,0.00000001,tCHZUST
|
||||
bitfinex,CLOUSD,crypto,Callisto-US Dollar,USD,1,0.00001,0.00000001,tCLOUSD
|
||||
bitfinex,CNDUSD,crypto,Cindicator-US Dollar,USD,1,0.00001,0.00000001,tCNDUSD
|
||||
bitfinex,CNHCNHT,crypto,CNH-Tether CNHt,CNHT,1,0.00001,0.00000001,tCNH:CNHT
|
||||
bitfinex,CNNUSD,crypto,Content Neutrality Network-US Dollar,USD,1,0.00001,0.00000001,tCNNUSD
|
||||
bitfinex,COMPUSD,crypto,Compound-US Dollar,USD,1,0.00001,0.00000001,tCOMP:USD
|
||||
bitfinex,COMPUSDT,crypto,Compound-Tether USDt,USDT,1,0.00001,0.00000001,tCOMP:UST
|
||||
bitfinex,CSUSD,crypto,Credits-US Dollar,USD,1,0.00001,0.00000001,tCSXUSD
|
||||
bitfinex,CTXCUSD,crypto,Cortex-US Dollar,USD,1,0.00001,0.00000001,tCTXUSD
|
||||
bitfinex,CTKUSD,crypto,CERTIK-US Dollar,USD,1,0.00001,0.00000001,tCTKUSD
|
||||
bitfinex,CTKUSDT,crypto,CERTIK-Tether USDt,USDT,1,0.00001,0.00000001,tCTKUST
|
||||
bitfinex,DAIBTC,crypto,Dai Stablecoin-Bitcoin,BTC,1,0.00001,0.00000001,tDAIBTC
|
||||
bitfinex,DAIETH,crypto,Dai Stablecoin-Ethereum,ETH,1,0.00001,0.00000001,tDAIETH
|
||||
bitfinex,DAIUSD,crypto,Dai Stablecoin-US Dollar,USD,1,0.00001,0.00000001,tDAIUSD
|
||||
bitfinex,DAPPUSD,crypto,DAPP-US Dollar,USD,1,0.00001,0.00000001,tDAPP:USD
|
||||
bitfinex,DAPPUSDT,crypto,DAPP-Tether USDt,USDT,1,0.00001,0.00000001,tDAPP:UST
|
||||
bitfinex,DATABTC,crypto,Streamr-Bitcoin,BTC,1,0.00001,0.00000001,tDATBTC
|
||||
bitfinex,DATAUSD,crypto,Streamr-US Dollar,USD,1,0.00001,0.00000001,tDATUSD
|
||||
bitfinex,DGBUSD,crypto,Digibyte-US Dollar,USD,1,0.00001,0.00000001,tDGBUSD
|
||||
bitfinex,DGXUSD,crypto,Digix Gold Token-US Dollar,USD,1,0.00001,0.00000001,tDGXUSD
|
||||
bitfinex,MDOGEBTC,crypto,MegaDOGE-Bitcoin,BTC,1,0.00001,0.00000001,tDOGBTC
|
||||
bitfinex,DOGEUSD,crypto,DOGE-US Dollar,USD,1,0.00001,0.00000001,tDOGE:USD
|
||||
bitfinex,DOGEUSDT,crypto,DOGE-Tether USDt,USDT,1,0.00001,0.00000001,tDOGE:UST
|
||||
bitfinex,MDOGEUSD,crypto,MegaDOGE-US Dollar,USD,1,0.00001,0.00000001,tDOGUSD
|
||||
bitfinex,MDOGEUSDT,crypto,MegaDOGE-Tether USDt,USDT,1,0.00001,0.00000001,tDOGUST
|
||||
bitfinex,DOTBTC,crypto,Polkadot-Bitcoin,BTC,1,0.00001,0.00000001,tDOTBTC
|
||||
bitfinex,DOTUSD,crypto,Polkadot-US Dollar,USD,1,0.00001,0.00000001,tDOTUSD
|
||||
bitfinex,DOTUSDT,crypto,Polkadot-Tether USDt,USDT,1,0.00001,0.00000001,tDOTUST
|
||||
bitfinex,DRGNUSD,crypto,Dragonchain-US Dollar,USD,1,0.00001,0.00000001,tDRNUSD
|
||||
bitfinex,DASHBTC,crypto,Dash-Bitcoin,BTC,1,0.00001,0.00000001,tDSHBTC
|
||||
bitfinex,DASHUSD,crypto,Dash-US Dollar,USD,1,0.00001,0.00000001,tDSHUSD
|
||||
bitfinex,DTAUSD,crypto,Data-US Dollar,USD,1,0.00001,0.00000001,tDTAUSD
|
||||
bitfinex,DTHUSD,crypto,Dether-US Dollar,USD,1,0.00001,0.00000001,tDTHUSD
|
||||
bitfinex,DTUSD,crypto,DragonToken-US Dollar,USD,1,0.00001,0.00000001,tDTXUSD
|
||||
bitfinex,DUSKBTC,crypto,Dusk Network-Bitcoin,BTC,1,0.00001,0.00000001,tDUSK:BTC
|
||||
bitfinex,DUSKUSD,crypto,Dusk Network-US Dollar,USD,1,0.00001,0.00000001,tDUSK:USD
|
||||
bitfinex,PNTBTC,crypto,pNetwork-Bitcoin,BTC,1,0.00001,0.00000001,tEDOBTC
|
||||
@@ -556,7 +555,6 @@ bitfinex,PNTETH,crypto,pNetwork-Ethereum,ETH,1,0.00001,0.00000001,tEDOETH
|
||||
bitfinex,PNTUSD,crypto,pNetwork-US Dollar,USD,1,0.00001,0.00000001,tEDOUSD
|
||||
bitfinex,EGLDUSD,crypto,Elrond-US Dollar,USD,1,0.00001,0.00000001,tEGLD:USD
|
||||
bitfinex,EGLDUSDT,crypto,Elrond-Tether USDt,USDT,1,0.00001,0.00000001,tEGLD:UST
|
||||
bitfinex,ELFUSD,crypto,aelf-US Dollar,USD,1,0.00001,0.00000001,tELFUSD
|
||||
bitfinex,ENJUSD,crypto,Enjin-US Dollar,USD,1,0.00001,0.00000001,tENJUSD
|
||||
bitfinex,EOSBTC,crypto,EOS-Bitcoin,BTC,1,0.00001,0.00000001,tEOSBTC
|
||||
bitfinex,EOSDTUSD,crypto,EOSDT-US Dollar,USD,1,0.00001,0.00000001,tEOSDT:USD
|
||||
@@ -570,6 +568,9 @@ bitfinex,EOSUSDT,crypto,EOS-Tether USDt,USDT,1,0.00001,0.00000001,tEOSUST
|
||||
bitfinex,ESSUSD,crypto,Essentia-US Dollar,USD,1,0.00001,0.00000001,tESSUSD
|
||||
bitfinex,ETCBTC,crypto,Ethereum Classic-Bitcoin,BTC,1,0.00001,0.00000001,tETCBTC
|
||||
bitfinex,ETCUSD,crypto,Ethereum Classic-US Dollar,USD,1,0.00001,0.00000001,tETCUSD
|
||||
bitfinex,ETH2ETH,crypto,ETH2X-Ethereum,ETH,1,0.00001,0.00000001,tETH2X:ETH
|
||||
bitfinex,ETH2USD,crypto,ETH2X-US Dollar,USD,1,0.00001,0.00000001,tETH2X:USD
|
||||
bitfinex,ETH2USDT,crypto,ETH2X-Tether USDt,USDT,1,0.00001,0.00000001,tETH2X:UST
|
||||
bitfinex,ETHBTC,crypto,Ethereum-Bitcoin,BTC,1,0.00001,0.00000001,tETHBTC
|
||||
bitfinex,ETHEUR,crypto,Ethereum-Euro,EUR,1,0.00001,0.00000001,tETHEUR
|
||||
bitfinex,ETHGBP,crypto,Ethereum-Pound Sterling,GBP,1,0.00001,0.00000001,tETHGBP
|
||||
@@ -582,37 +583,49 @@ bitfinex,ETPUSD,crypto,ETP-US Dollar,USD,1,0.00001,0.00000001,tETPUSD
|
||||
bitfinex,EURSUSD,crypto,EURS-US Dollar,USD,1,0.00001,0.00000001,tEUSUSD
|
||||
bitfinex,EURTEUR,crypto,Tether EURt-Euro,EUR,1,0.00001,0.00000001,tEUTEUR
|
||||
bitfinex,EURTUSD,crypto,Tether EURt-US Dollar,USD,1,0.00001,0.00000001,tEUTUSD
|
||||
bitfinex,EVTUSD,crypto,Ethfinex Voting Token-US Dollar,USD,1,0.00001,0.00000001,tEVTUSD
|
||||
bitfinex,EURTUSDT,crypto,Tether EURt-Tether USDt,USDT,1,0.00001,0.00000001,tEUTUST
|
||||
bitfinex,EXRDBTC,crypto,E-RADIX-Bitcoin,BTC,1,0.00001,0.00000001,tEXRD:BTC
|
||||
bitfinex,EXRDUSD,crypto,E-RADIX-US Dollar,USD,1,0.00001,0.00000001,tEXRD:USD
|
||||
bitfinex,FCLUSD,crypto,Fractal-US Dollar,USD,1,0.00001,0.00000001,tFCLUSD
|
||||
bitfinex,FCLUSDT,crypto,Fractal-Tether USDt,USDT,1,0.00001,0.00000001,tFCLUST
|
||||
bitfinex,FETUSD,crypto,Fetch.AI-US Dollar,USD,1,0.00001,0.00000001,tFETUSD
|
||||
bitfinex,FETUSDT,crypto,Fetch.AI-Tether USDt,USDT,1,0.00001,0.00000001,tFETUST
|
||||
bitfinex,FILUSD,crypto,Filecoin-US Dollar,USD,1,0.00001,0.00000001,tFILUSD
|
||||
bitfinex,FILUSDT,crypto,Filecoin-Tether USDt,USDT,1,0.00001,0.00000001,tFILUST
|
||||
bitfinex,FOAUSD,crypto,FOAM-US Dollar,USD,1,0.00001,0.00000001,tFOAUSD
|
||||
bitfinex,FSNUSD,crypto,Fusion-US Dollar,USD,1,0.00001,0.00000001,tFSNUSD
|
||||
bitfinex,FORTHUSD,crypto,FORTH-US Dollar,USD,1,0.00001,0.00000001,tFORTH:USD
|
||||
bitfinex,FORTHUSDT,crypto,FORTH-Tether USDt,USDT,1,0.00001,0.00000001,tFORTH:UST
|
||||
bitfinex,FTMUSD,crypto,Fantom-US Dollar,USD,1,0.00001,0.00000001,tFTMUSD
|
||||
bitfinex,FTMUSDT,crypto,Fantom-Tether USDt,USDT,1,0.00001,0.00000001,tFTMUST
|
||||
bitfinex,FTTUSD,crypto,FTX.com-US Dollar,USD,1,0.00001,0.00000001,tFTTUSD
|
||||
bitfinex,FTTUSDT,crypto,FTX.com-Tether USDt,USDT,1,0.00001,0.00000001,tFTTUST
|
||||
bitfinex,FUNUSD,crypto,FunFair-US Dollar,USD,1,0.00001,0.00000001,tFUNUSD
|
||||
bitfinex,GENUSD,crypto,DAOstack-US Dollar,USD,1,0.00001,0.00000001,tGENUSD
|
||||
bitfinex,GNOUSD,crypto,Gnosis-US Dollar,USD,1,0.00001,0.00000001,tGNOUSD
|
||||
bitfinex,GNTBTC,crypto,Golem-Bitcoin,BTC,1,0.00001,0.00000001,tGNTBTC
|
||||
bitfinex,GNTETH,crypto,Golem-Ethereum,ETH,1,0.00001,0.00000001,tGNTETH
|
||||
bitfinex,GNTUSD,crypto,Golem-US Dollar,USD,1,0.00001,0.00000001,tGNTUSD
|
||||
bitfinex,GLMUSD,crypto,Golem-US Dollar,USD,1,0.00001,0.00000001,tGNTUSD
|
||||
bitfinex,GOTEUR,crypto,ParkinGO-Euro,EUR,1,0.00001,0.00000001,tGOTEUR
|
||||
bitfinex,GOTUSD,crypto,ParkinGO-US Dollar,USD,1,0.00001,0.00000001,tGOTUSD
|
||||
bitfinex,GUSDUSD,crypto,GUSD-US Dollar,USD,1,0.00001,0.00000001,tGSDUSD
|
||||
bitfinex,GRTUSD,crypto,The Graph-US Dollar,USD,1,0.00001,0.00000001,tGRTUSD
|
||||
bitfinex,GRTUSDT,crypto,The Graph-Tether USDt,USDT,1,0.00001,0.00000001,tGRTUST
|
||||
bitfinex,GTXUSD,crypto,Gate.IO-US Dollar,USD,1,0.00001,0.00000001,tGTXUSD
|
||||
bitfinex,GTXUSDT,crypto,Gate.IO-Tether USDt,USDT,1,0.00001,0.00000001,tGTXUST
|
||||
bitfinex,HOTUSD,crypto,Hydro Protocol-US Dollar,USD,1,0.00001,0.00000001,tHOTUSD
|
||||
bitfinex,IMPUSD,crypto,Ether Kingdoms-US Dollar,USD,1,0.00001,0.00000001,tIMPUSD
|
||||
bitfinex,IOSTUSD,crypto,IOSToken-US Dollar,USD,1,0.00001,0.00000001,tIOSUSD
|
||||
bitfinex,HEZUSD,crypto,Hermez-US Dollar,USD,1,0.00001,0.00000001,tHEZUSD
|
||||
bitfinex,HEZUSDT,crypto,Hermez-Tether USDt,USDT,1,0.00001,0.00000001,tHEZUST
|
||||
bitfinex,ICEUSD,crypto,ICE-US Dollar,USD,1,0.00001,0.00000001,tICEUSD
|
||||
bitfinex,ICPBTC,crypto,Dfinity-Bitcoin,BTC,1,0.00001,0.00000001,tICPBTC
|
||||
bitfinex,ICPUSD,crypto,Dfinity-US Dollar,USD,1,0.00001,0.00000001,tICPUSD
|
||||
bitfinex,ICPUSDT,crypto,Dfinity-Tether USDt,USDT,1,0.00001,0.00000001,tICPUST
|
||||
bitfinex,IDUSD,crypto,Everest (ID)-US Dollar,USD,1,0.00001,0.00000001,tIDXUSD
|
||||
bitfinex,IDUSDT,crypto,Everest (ID)-Tether USDt,USDT,1,0.00001,0.00000001,tIDXUST
|
||||
bitfinex,IOTABTC,crypto,Iota-Bitcoin,BTC,1,0.00001,0.00000001,tIOTBTC
|
||||
bitfinex,IOTAETH,crypto,Iota-Ethereum,ETH,1,0.00001,0.00000001,tIOTETH
|
||||
bitfinex,IOTAEUR,crypto,Iota-Euro,EUR,1,0.00001,0.00000001,tIOTEUR
|
||||
bitfinex,IOTAGBP,crypto,Iota-Pound Sterling,GBP,1,0.00001,0.00000001,tIOTGBP
|
||||
bitfinex,IOTAJPY,crypto,Iota-Japanese Yen,JPY,1,0.00001,0.00000001,tIOTJPY
|
||||
bitfinex,IOTAUSD,crypto,Iota-US Dollar,USD,1,0.00001,0.00000001,tIOTUSD
|
||||
bitfinex,IQXEOS,crypto,Everipedia-EOS,EOS,1,0.00001,0.00000001,tIQXEOS
|
||||
bitfinex,IQXUSD,crypto,Everipedia-US Dollar,USD,1,0.00001,0.00000001,tIQXUSD
|
||||
bitfinex,IQXUSDT,crypto,Everipedia-Tether USDt,USDT,1,0.00001,0.00000001,tIQXUST
|
||||
bitfinex,JSTBTC,crypto,JST-Bitcoin,BTC,1,0.00001,0.00000001,tJSTBTC
|
||||
bitfinex,JSTUSD,crypto,JST-US Dollar,USD,1,0.00001,0.00000001,tJSTUSD
|
||||
bitfinex,JSTUSDT,crypto,JST-Tether USDt,USDT,1,0.00001,0.00000001,tJSTUST
|
||||
bitfinex,KANUSD,crypto,BitKan-US Dollar,USD,1,0.00001,0.00000001,tKANUSD
|
||||
bitfinex,KANUSDT,crypto,BitKan-Tether USDt,USDT,1,0.00001,0.00000001,tKANUST
|
||||
bitfinex,KNCBTC,crypto,Kyber-Bitcoin,BTC,1,0.00001,0.00000001,tKNCBTC
|
||||
@@ -626,25 +639,23 @@ bitfinex,LEOUSD,crypto,Unus Sed LEO-US Dollar,USD,1,0.00001,0.00000001,tLEOUSD
|
||||
bitfinex,LEOUSDT,crypto,Unus Sed LEO-Tether USDt,USDT,1,0.00001,0.00000001,tLEOUST
|
||||
bitfinex,LINKUSD,crypto,ChainLink-US Dollar,USD,1,0.00001,0.00000001,tLINK:USD
|
||||
bitfinex,LINKUSDT,crypto,ChainLink-Tether USDt,USDT,1,0.00001,0.00000001,tLINK:UST
|
||||
bitfinex,LOOUSD,crypto,Loom-US Dollar,USD,1,0.00001,0.00000001,tLOOUSD
|
||||
bitfinex,LRCBTC,crypto,Loopring-Bitcoin,BTC,1,0.00001,0.00000001,tLRCBTC
|
||||
bitfinex,LRCUSD,crypto,Loopring-US Dollar,USD,1,0.00001,0.00000001,tLRCUSD
|
||||
bitfinex,LTCBTC,crypto,Litecoin-Bitcoin,BTC,1,0.00001,0.00000001,tLTCBTC
|
||||
bitfinex,LTCUSD,crypto,Litecoin-US Dollar,USD,1,0.00001,0.00000001,tLTCUSD
|
||||
bitfinex,LTCUSDT,crypto,Litecoin-Tether USDt,USDT,1,0.00001,0.00000001,tLTCUST
|
||||
bitfinex,LUNAUSD,crypto,LUNA-US Dollar,USD,1,0.00001,0.00000001,tLUNA:USD
|
||||
bitfinex,LUNAUSDT,crypto,LUNA-Tether USDt,USDT,1,0.00001,0.00000001,tLUNA:UST
|
||||
bitfinex,LYMUSD,crypto,Lympo-US Dollar,USD,1,0.00001,0.00000001,tLYMUSD
|
||||
bitfinex,MANUSD,crypto,Matrix-US Dollar,USD,1,0.00001,0.00000001,tMANUSD
|
||||
bitfinex,MGOUSD,crypto,MobileGo-US Dollar,USD,1,0.00001,0.00000001,tMGOUSD
|
||||
bitfinex,MITHUSD,crypto,Mithril-US Dollar,USD,1,0.00001,0.00000001,tMITUSD
|
||||
bitfinex,MKRBTC,crypto,Maker-Bitcoin,BTC,1,0.00001,0.00000001,tMKRBTC
|
||||
bitfinex,MKRETH,crypto,Maker-Ethereum,ETH,1,0.00001,0.00000001,tMKRETH
|
||||
bitfinex,MIRUSD,crypto,Mirror Protocol-US Dollar,USD,1,0.00001,0.00000001,tMIRUSD
|
||||
bitfinex,MIRUSDT,crypto,Mirror Protocol-Tether USDt,USDT,1,0.00001,0.00000001,tMIRUST
|
||||
bitfinex,MKRUSD,crypto,Maker-US Dollar,USD,1,0.00001,0.00000001,tMKRUSD
|
||||
bitfinex,MLNUSD,crypto,Melon-US Dollar,USD,1,0.00001,0.00000001,tMLNUSD
|
||||
bitfinex,MANABTC,crypto,Decentraland-Bitcoin,BTC,1,0.00001,0.00000001,tMNABTC
|
||||
bitfinex,MANAUSD,crypto,Decentraland-US Dollar,USD,1,0.00001,0.00000001,tMNAUSD
|
||||
bitfinex,MTNUSD,crypto,Medicalchain-US Dollar,USD,1,0.00001,0.00000001,tMTNUSD
|
||||
bitfinex,NCASHUSD,crypto,Nucleus Vision-US Dollar,USD,1,0.00001,0.00000001,tNCAUSD
|
||||
bitfinex,NECETH,crypto,Ethfinex Nectar Token-Ethereum,ETH,1,0.00001,0.00000001,tNECETH
|
||||
bitfinex,MOBUSD,crypto,Mobilecoin-US Dollar,USD,1,0.00001,0.00000001,tMOBUSD
|
||||
bitfinex,MOBUSDT,crypto,Mobilecoin-Tether USDt,USDT,1,0.00001,0.00000001,tMOBUST
|
||||
bitfinex,NEARUSD,crypto,Near-US Dollar,USD,1,0.00001,0.00000001,tNEAR:USD
|
||||
bitfinex,NEARUSDT,crypto,Near-Tether USDt,USDT,1,0.00001,0.00000001,tNEAR:UST
|
||||
bitfinex,NECUSD,crypto,Ethfinex Nectar Token-US Dollar,USD,1,0.00001,0.00000001,tNECUSD
|
||||
bitfinex,NEOBTC,crypto,NEO-Bitcoin,BTC,1,0.00001,0.00000001,tNEOBTC
|
||||
bitfinex,NEOETH,crypto,NEO-Ethereum,ETH,1,0.00001,0.00000001,tNEOETH
|
||||
@@ -652,59 +663,59 @@ bitfinex,NEOEUR,crypto,NEO-Euro,EUR,1,0.00001,0.00000001,tNEOEUR
|
||||
bitfinex,NEOGBP,crypto,NEO-Pound Sterling,GBP,1,0.00001,0.00000001,tNEOGBP
|
||||
bitfinex,NEOJPY,crypto,NEO-Japanese Yen,JPY,1,0.00001,0.00000001,tNEOJPY
|
||||
bitfinex,NEOUSD,crypto,NEO-US Dollar,USD,1,0.00001,0.00000001,tNEOUSD
|
||||
bitfinex,NUTUSD,crypto,Native Utility Token-US Dollar,USD,1,0.00001,0.00000001,tNUTUSD
|
||||
bitfinex,NUTUSDT,crypto,Native Utility Token-Tether USDt,USDT,1,0.00001,0.00000001,tNUTUST
|
||||
bitfinex,NEXOBTC,crypto,NEXO-Bitcoin,BTC,1,0.00001,0.00000001,tNEXO:BTC
|
||||
bitfinex,NEXOUSD,crypto,NEXO-US Dollar,USD,1,0.00001,0.00000001,tNEXO:USD
|
||||
bitfinex,NEXOUSDT,crypto,NEXO-Tether USDt,USDT,1,0.00001,0.00000001,tNEXO:UST
|
||||
bitfinex,OCEANUSD,crypto,OCEAN protocol-US Dollar,USD,1,0.00001,0.00000001,tOCEAN:USD
|
||||
bitfinex,OCEANUSDT,crypto,OCEAN protocol-Tether USDt,USDT,1,0.00001,0.00000001,tOCEAN:UST
|
||||
bitfinex,ODEUSD,crypto,Odem-US Dollar,USD,1,0.00001,0.00000001,tODEUSD
|
||||
bitfinex,OKBUSD,crypto,OKB-US Dollar,USD,1,0.00001,0.00000001,tOKBUSD
|
||||
bitfinex,OKBUSDT,crypto,OKB-Tether USDt,USDT,1,0.00001,0.00000001,tOKBUST
|
||||
bitfinex,OMGBTC,crypto,OmiseGO-Bitcoin,BTC,1,0.00001,0.00000001,tOMGBTC
|
||||
bitfinex,OMGETH,crypto,OmiseGO-Ethereum,ETH,1,0.00001,0.00000001,tOMGETH
|
||||
bitfinex,OMGUSD,crypto,OmiseGO-US Dollar,USD,1,0.00001,0.00000001,tOMGUSD
|
||||
bitfinex,OMNIBTC,crypto,Omni-Bitcoin,BTC,1,0.00001,0.00000001,tOMNBTC
|
||||
bitfinex,OMNIUSD,crypto,Omni-US Dollar,USD,1,0.00001,0.00000001,tOMNUSD
|
||||
bitfinex,ONLUSD,crypto,On.Live-US Dollar,USD,1,0.00001,0.00000001,tONLUSD
|
||||
bitfinex,ORSUSD,crypto,ORS-US Dollar,USD,1,0.00001,0.00000001,tORSUSD
|
||||
bitfinex,PAIUSD,crypto,PAI Project-US Dollar,USD,1,0.00001,0.00000001,tPAIUSD
|
||||
bitfinex,OXYUSD,crypto,Oxygen-US Dollar,USD,1,0.00001,0.00000001,tOXYUSD
|
||||
bitfinex,OXYUSDT,crypto,Oxygen-Tether USDt,USDT,1,0.00001,0.00000001,tOXYUST
|
||||
bitfinex,PASSUSD,crypto,Blockpass-US Dollar,USD,1,0.00001,0.00000001,tPASUSD
|
||||
bitfinex,PAXUSD,crypto,Paxos-US Dollar,USD,1,0.00001,0.00000001,tPAXUSD
|
||||
bitfinex,PAXUSDT,crypto,Paxos-Tether USDt,USDT,1,0.00001,0.00000001,tPAXUST
|
||||
bitfinex,PLANETSUSD,crypto,PLANETS-US Dollar,USD,1,0.00001,0.00000001,tPLANETS:USD
|
||||
bitfinex,PLANETSUSDT,crypto,PLANETS-Tether USDt,USDT,1,0.00001,0.00000001,tPLANETS:UST
|
||||
bitfinex,PLUUSD,crypto,Pluton-US Dollar,USD,1,0.00001,0.00000001,tPLUUSD
|
||||
bitfinex,PNKETH,crypto,Kleros-Ethereum,ETH,1,0.00001,0.00000001,tPNKETH
|
||||
bitfinex,PNKUSD,crypto,Kleros-US Dollar,USD,1,0.00001,0.00000001,tPNKUSD
|
||||
bitfinex,POAUSD,crypto,POA Network (erc20)-US Dollar,USD,1,0.00001,0.00000001,tPOAUSD
|
||||
bitfinex,POLYUSD,crypto,Polymath-US Dollar,USD,1,0.00001,0.00000001,tPOYUSD
|
||||
bitfinex,QASHUSD,crypto,QASH-US Dollar,USD,1,0.00001,0.00000001,tQSHUSD
|
||||
bitfinex,QTFBTC,crypto,Quantfury-Bitcoin,BTC,1,0.00001,0.00000001,tQTFBTC
|
||||
bitfinex,QTFUSD,crypto,Quantfury-US Dollar,USD,1,0.00001,0.00000001,tQTFUSD
|
||||
bitfinex,QTUMBTC,crypto,Qtum-Bitcoin,BTC,1,0.00001,0.00000001,tQTMBTC
|
||||
bitfinex,QTUMUSD,crypto,Qtum-US Dollar,USD,1,0.00001,0.00000001,tQTMUSD
|
||||
bitfinex,RBTCBTC,crypto,RBTC-Bitcoin,BTC,1,0.00001,0.00000001,tRBTBTC
|
||||
bitfinex,RBTCUSD,crypto,RBTC-US Dollar,USD,1,0.00001,0.00000001,tRBTUSD
|
||||
bitfinex,RCNUSD,crypto,RCN-US Dollar,USD,1,0.00001,0.00000001,tRCNUSD
|
||||
bitfinex,RDNUSD,crypto,Raiden-US Dollar,USD,1,0.00001,0.00000001,tRDNUSD
|
||||
bitfinex,REP2BTC,crypto,Augur-Bitcoin,BTC,1,0.00001,0.00000001,tREPBTC
|
||||
bitfinex,REP2USD,crypto,Augur-US Dollar,USD,1,0.00001,0.00000001,tREPUSD
|
||||
bitfinex,REQUSD,crypto,Request Network-US Dollar,USD,1,0.00001,0.00000001,tREQUSD
|
||||
bitfinex,RIFUSD,crypto,RIF-US Dollar,USD,1,0.00001,0.00000001,tRIFUSD
|
||||
bitfinex,RINGXUSD,crypto,RingX-US Dollar,USD,1,0.00001,0.00000001,tRINGX:USD
|
||||
bitfinex,RLCBTC,crypto,iExec-Bitcoin,BTC,1,0.00001,0.00000001,tRLCBTC
|
||||
bitfinex,RLCUSD,crypto,iExec-US Dollar,USD,1,0.00001,0.00000001,tRLCUSD
|
||||
bitfinex,RRBUSD,crypto,RenrenBit-US Dollar,USD,1,0.00001,0.00000001,tRRBUSD
|
||||
bitfinex,RRBUSDT,crypto,RenrenBit-Tether USDt,USDT,1,0.00001,0.00000001,tRRBUST
|
||||
bitfinex,RRTUSD,crypto,Recovery Right Tokens-US Dollar,USD,1,0.00001,0.00000001,tRRTUSD
|
||||
bitfinex,RTEUSD,crypto,Rate3-US Dollar,USD,1,0.00001,0.00000001,tRTEUSD
|
||||
bitfinex,SANBTC,crypto,Santiment-Bitcoin,BTC,1,0.00001,0.00000001,tSANBTC
|
||||
bitfinex,SANETH,crypto,Santiment-Ethereum,ETH,1,0.00001,0.00000001,tSANETH
|
||||
bitfinex,SANUSD,crypto,Santiment-US Dollar,USD,1,0.00001,0.00000001,tSANUSD
|
||||
bitfinex,XDUSD,crypto,Data Transaction Token-US Dollar,USD,1,0.00001,0.00000001,tSCRUSD
|
||||
bitfinex,SEEUSD,crypto,Seer-US Dollar,USD,1,0.00001,0.00000001,tSEEUSD
|
||||
bitfinex,SNGLSUSD,crypto,SingularDTV-US Dollar,USD,1,0.00001,0.00000001,tSNGUSD
|
||||
bitfinex,SNTUSD,crypto,Status-US Dollar,USD,1,0.00001,0.00000001,tSNTUSD
|
||||
bitfinex,SNXUSD,crypto,Synthetix Network-US Dollar,USD,1,0.00001,0.00000001,tSNXUSD
|
||||
bitfinex,SNXUSDT,crypto,Synthetix Network-Tether USDt,USDT,1,0.00001,0.00000001,tSNXUST
|
||||
bitfinex,SPANKUSD,crypto,SpankChain-US Dollar,USD,1,0.00001,0.00000001,tSPKUSD
|
||||
bitfinex,SOLUSD,crypto,Solana-US Dollar,USD,1,0.00001,0.00000001,tSOLUSD
|
||||
bitfinex,SOLUSDT,crypto,Solana-Tether USDt,USDT,1,0.00001,0.00000001,tSOLUST
|
||||
bitfinex,STORJUSD,crypto,Storj-US Dollar,USD,1,0.00001,0.00000001,tSTJUSD
|
||||
bitfinex,SWMUSD,crypto,Swarm-US Dollar,USD,1,0.00001,0.00000001,tSWMUSD
|
||||
bitfinex,TKNUSD,crypto,TokenCard-US Dollar,USD,1,0.00001,0.00000001,tTKNUSD
|
||||
bitfinex,TNBUSD,crypto,Time New Bank-US Dollar,USD,1,0.00001,0.00000001,tTNBUSD
|
||||
bitfinex,TRIUSD,crypto,Tripio-US Dollar,USD,1,0.00001,0.00000001,tTRIUSD
|
||||
bitfinex,SUKUUSD,crypto,SUKU-US Dollar,USD,1,0.00001,0.00000001,tSUKU:USD
|
||||
bitfinex,SUKUUSDT,crypto,SUKU-Tether USDt,USDT,1,0.00001,0.00000001,tSUKU:UST
|
||||
bitfinex,SUNUSD,crypto,SUN-US Dollar,USD,1,0.00001,0.00000001,tSUNUSD
|
||||
bitfinex,SUNUSDT,crypto,SUN-Tether USDt,USDT,1,0.00001,0.00000001,tSUNUST
|
||||
bitfinex,SUSHIUSD,crypto,SUSHI-US Dollar,USD,1,0.00001,0.00000001,tSUSHI:USD
|
||||
bitfinex,SUSHIUSDT,crypto,SUSHI-Tether USDt,USDT,1,0.00001,0.00000001,tSUSHI:UST
|
||||
bitfinex,TERRAUSTUSD,crypto,TerraUST-US Dollar,USD,1,0.00001,0.00000001,tTERRAUST:USD
|
||||
bitfinex,TERRAUSTUSDT,crypto,TerraUST-Tether USDt,USDT,1,0.00001,0.00000001,tTERRAUST:UST
|
||||
bitfinex,TRXBTC,crypto,TRON-Bitcoin,BTC,1,0.00001,0.00000001,tTRXBTC
|
||||
bitfinex,TRXETH,crypto,TRON-Ethereum,ETH,1,0.00001,0.00000001,tTRXETH
|
||||
bitfinex,TRXEUR,crypto,TRON-Euro,EUR,1,0.00001,0.00000001,tTRXEUR
|
||||
@@ -713,51 +724,52 @@ bitfinex,TUSDUSD,crypto,TrueUSD-US Dollar,USD,1,0.00001,0.00000001,tTSDUSD
|
||||
bitfinex,TUSDUSDT,crypto,TrueUSD-Tether USDt,USDT,1,0.00001,0.00000001,tTSDUST
|
||||
bitfinex,USDCUSD,crypto,USDc-US Dollar,USD,1,0.00001,0.00000001,tUDCUSD
|
||||
bitfinex,USDCUSDT,crypto,USDc-Tether USDt,USDT,1,0.00001,0.00000001,tUDCUST
|
||||
bitfinex,UFRUSD,crypto,Upfiring-US Dollar,USD,1,0.00001,0.00000001,tUFRUSD
|
||||
bitfinex,UNIUSD,crypto,Uniswap-US Dollar,USD,1,0.00001,0.00000001,tUNIUSD
|
||||
bitfinex,UNIUSDT,crypto,Uniswap-Tether USDt,USDT,1,0.00001,0.00000001,tUNIUST
|
||||
bitfinex,UOPUSD,crypto,Utopia-US Dollar,USD,1,0.00001,0.00000001,tUOPUSD
|
||||
bitfinex,UOPUSDT,crypto,Utopia-Tether USDt,USDT,1,0.00001,0.00000001,tUOPUST
|
||||
bitfinex,UOSBTC,crypto,Ultra-Bitcoin,BTC,1,0.00001,0.00000001,tUOSBTC
|
||||
bitfinex,UOSUSD,crypto,Ultra-US Dollar,USD,1,0.00001,0.00000001,tUOSUSD
|
||||
bitfinex,USDKUSD,crypto,USDk-US Dollar,USD,1,0.00001,0.00000001,tUSKUSD
|
||||
bitfinex,USDTCNHT,crypto,Tether USDt-Tether CNHt,CNHT,1,0.00001,0.00000001,tUST:CNHT
|
||||
bitfinex,USDTUSD,crypto,Tether USDt-US Dollar,USD,1,0.00001,0.00000001,tUSTUSD
|
||||
bitfinex,UTKUSD,crypto,UTRUST-US Dollar,USD,1,0.00001,0.00000001,tUTKUSD
|
||||
bitfinex,UTNPUSD,crypto,Universa-US Dollar,USD,1,0.00001,0.00000001,tUTNUSD
|
||||
bitfinex,VEEUSD,crypto,BLOCKv-US Dollar,USD,1,0.00001,0.00000001,tVEEUSD
|
||||
bitfinex,VELOUSD,crypto,VELO-US Dollar,USD,1,0.00001,0.00000001,tVELO:USD
|
||||
bitfinex,VELOUSDT,crypto,VELO-Tether USDt,USDT,1,0.00001,0.00000001,tVELO:UST
|
||||
bitfinex,VETBTC,crypto,VeChain-Bitcoin,BTC,1,0.00001,0.00000001,tVETBTC
|
||||
bitfinex,VETUSD,crypto,VeChain-US Dollar,USD,1,0.00001,0.00000001,tVETUSD
|
||||
bitfinex,VLDUSD,crypto,Vetri-US Dollar,USD,1,0.00001,0.00000001,tVLDUSD
|
||||
bitfinex,VSYSBTC,crypto,V-SYSTEMS-Bitcoin,BTC,1,0.00001,0.00000001,tVSYBTC
|
||||
bitfinex,VSYSUSD,crypto,V-SYSTEMS-US Dollar,USD,1,0.00001,0.00000001,tVSYUSD
|
||||
bitfinex,WAXUSD,crypto,WAX-US Dollar,USD,1,0.00001,0.00000001,tWAXUSD
|
||||
bitfinex,WBTCETH,crypto,Wrapped Bitcoin-Ethereum,ETH,1,0.00001,0.00000001,tWBTETH
|
||||
bitfinex,WBTCUSD,crypto,Wrapped Bitcoin-US Dollar,USD,1,0.00001,0.00000001,tWBTUSD
|
||||
bitfinex,WPRUSD,crypto,WePower-US Dollar,USD,1,0.00001,0.00000001,tWPRUSD
|
||||
bitfinex,WTCUSD,crypto,Walton-US Dollar,USD,1,0.00001,0.00000001,tWTCUSD
|
||||
bitfinex,XAUTBTC,crypto,Tether XAUt-Bitcoin,BTC,1,0.00001,0.00000001,tXAUT:BTC
|
||||
bitfinex,XAUTUSD,crypto,Tether XAUt-US Dollar,USD,1,0.00001,0.00000001,tXAUT:USD
|
||||
bitfinex,XAUTUSDT,crypto,Tether XAUt-Tether USDt,USDT,1,0.00001,0.00000001,tXAUT:UST
|
||||
bitfinex,XCHFETH,crypto,CryptoFranc-Ethereum,ETH,1,0.00001,0.00000001,tXCHETH
|
||||
bitfinex,XCHFUSD,crypto,CryptoFranc-US Dollar,USD,1,0.00001,0.00000001,tXCHUSD
|
||||
bitfinex,XDCUSD,crypto,XinFin-US Dollar,USD,1,0.00001,0.00000001,tXDCUSD
|
||||
bitfinex,XDCUSDT,crypto,XinFin-Tether USDt,USDT,1,0.00001,0.00000001,tXDCUST
|
||||
bitfinex,XLMBTC,crypto,Stellar Lumen-Bitcoin,BTC,1,0.00001,0.00000001,tXLMBTC
|
||||
bitfinex,XLMETH,crypto,Stellar Lumen-Ethereum,ETH,1,0.00001,0.00000001,tXLMETH
|
||||
bitfinex,XLMUSD,crypto,Stellar Lumen-US Dollar,USD,1,0.00001,0.00000001,tXLMUSD
|
||||
bitfinex,XLMUSDT,crypto,Stellar Lumen-Tether USDt,USDT,1,0.00001,0.00000001,tXLMUST
|
||||
bitfinex,XMRBTC,crypto,Monero-Bitcoin,BTC,1,0.00001,0.00000001,tXMRBTC
|
||||
bitfinex,XMRUSD,crypto,Monero-US Dollar,USD,1,0.00001,0.00000001,tXMRUSD
|
||||
bitfinex,XMRUSDT,crypto,Monero-Tether USDt,USDT,1,0.00001,0.00000001,tXMRUST
|
||||
bitfinex,XRAUSD,crypto,Xriba-US Dollar,USD,1,0.00001,0.00000001,tXRAUSD
|
||||
bitfinex,XRPBTC,crypto,Ripple-Bitcoin,BTC,1,0.00001,0.00000001,tXRPBTC
|
||||
bitfinex,XRPUSD,crypto,Ripple-US Dollar,USD,1,0.00001,0.00000001,tXRPUSD
|
||||
bitfinex,XRPUSDT,crypto,Ripple-Tether USDt,USDT,1,0.00001,0.00000001,tXRPUST
|
||||
bitfinex,XSNUSD,crypto,Stakenet-US Dollar,USD,1,0.00001,0.00000001,tXSNUSD
|
||||
bitfinex,XTZBTC,crypto,Tezos-Bitcoin,BTC,1,0.00001,0.00000001,tXTZBTC
|
||||
bitfinex,XTZUSD,crypto,Tezos-US Dollar,USD,1,0.00001,0.00000001,tXTZUSD
|
||||
bitfinex,XVGUSD,crypto,Verge-US Dollar,USD,1,0.00001,0.00000001,tXVGUSD
|
||||
bitfinex,YFIUSD,crypto,Yearn.Finance-US Dollar,USD,1,0.00001,0.00000001,tYFIUSD
|
||||
bitfinex,YFIUSDT,crypto,Yearn.Finance-Tether USDt,USDT,1,0.00001,0.00000001,tYFIUST
|
||||
bitfinex,YEEDUSD,crypto,Yggdrash-US Dollar,USD,1,0.00001,0.00000001,tYGGUSD
|
||||
bitfinex,YOYOWUSD,crypto,YOYOW-US Dollar,USD,1,0.00001,0.00000001,tYYWUSD
|
||||
bitfinex,ZBTUSD,crypto,ZB Token-US Dollar,USD,1,0.00001,0.00000001,tZBTUSD
|
||||
bitfinex,MCSUSD,crypto,MCS-US Dollar,USD,1,0.00001,0.00000001,tYGGUSD
|
||||
bitfinex,ZCNUSD,crypto,0Chain-US Dollar,USD,1,0.00001,0.00000001,tZCNUSD
|
||||
bitfinex,ZECBTC,crypto,Zcash-Bitcoin,BTC,1,0.00001,0.00000001,tZECBTC
|
||||
bitfinex,ZECUSD,crypto,Zcash-US Dollar,USD,1,0.00001,0.00000001,tZECUSD
|
||||
bitfinex,ZILBTC,crypto,Zilliqa-Bitcoin,BTC,1,0.00001,0.00000001,tZILBTC
|
||||
bitfinex,ZILUSD,crypto,Zilliqa-US Dollar,USD,1,0.00001,0.00000001,tZILUSD
|
||||
bitfinex,ZRXBTC,crypto,0x-Bitcoin,BTC,1,0.00001,0.00000001,tZRXBTC
|
||||
bitfinex,ZRXETH,crypto,0x-Ethereum,ETH,1,0.00001,0.00000001,tZRXETH
|
||||
|
||||
|
@@ -55,7 +55,7 @@ namespace QuantConnect.Tests.Brokerages.GDAX
|
||||
|
||||
public static WebSocketMessage GetArgs(string json)
|
||||
{
|
||||
return new WebSocketMessage(json);
|
||||
return new WebSocketMessage(null, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user