Files
quantconnect--lean/ToolBox/CoinApi/CoinApiDataQueueHandler.cs
Stefano Raggi 062b27e0b0 Bitfinex DataQueueHandler June 2019 Update
- Moved IConnectionHandler and DefaultConnectionHandler from ToolBox project to Brokerages project
- Added connectionId to IConnectionHandler.Initialize to support multiple connections
- Added new BitfinexSubscriptionManager to BitfinexBrokerage to handle max 30 channels per websocket
2019-07-02 15:17:06 +02:00

454 lines
17 KiB
C#

/*
* 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 Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using QuantConnect.Brokerages;
using QuantConnect.Configuration;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Interfaces;
using QuantConnect.Logging;
using QuantConnect.Packets;
using QuantConnect.ToolBox.CoinApi.Messages;
using QuantConnect.Util;
namespace QuantConnect.ToolBox.CoinApi
{
/// <summary>
/// An implementation of <see cref="IDataQueueHandler"/> for CoinAPI
/// </summary>
public class CoinApiDataQueueHandler : IDataQueueHandler, IDisposable
{
private const string WebSocketUrl = "wss://ws.coinapi.io/v1/";
private readonly string _apiKey = Config.Get("coinapi-api-key");
private readonly WebSocketWrapper _webSocket = new WebSocketWrapper();
private readonly object _locker = new object();
private readonly List<Tick> _ticks = new List<Tick>();
private readonly DefaultConnectionHandler _connectionHandler = new DefaultConnectionHandler();
private readonly CoinApiSymbolMapper _symbolMapper = new CoinApiSymbolMapper();
private readonly TimeSpan _subscribeDelay = TimeSpan.FromMilliseconds(250);
private readonly object _lockerSubscriptions = new object();
private HashSet<Symbol> _subscribedSymbols = new HashSet<Symbol>();
private DateTime _lastSubscribeRequestUtcTime = DateTime.MinValue;
private bool _subscriptionsPending;
private readonly TimeSpan _minimumTimeBetweenHelloMessages = TimeSpan.FromSeconds(5);
private DateTime _nextHelloMessageUtcTime = DateTime.MinValue;
private List<string> _subscribedExchanges = new List<string>();
private readonly Dictionary<Symbol, Tick> _previousQuotes = new Dictionary<Symbol, Tick>();
private readonly Queue<WebSocketMessage> _processingQueue = new Queue<WebSocketMessage>();
private readonly AutoResetEvent _processingEvent = new AutoResetEvent(false);
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
/// <summary>
/// Initializes a new instance of the <see cref="CoinApiDataQueueHandler"/> class
/// </summary>
public CoinApiDataQueueHandler()
{
_connectionHandler.ConnectionLost += OnConnectionLost;
_connectionHandler.ConnectionRestored += OnConnectionRestored;
_connectionHandler.ReconnectRequested += OnReconnectRequested;
_connectionHandler.Initialize(string.Empty);
_webSocket.Initialize(WebSocketUrl);
_webSocket.Message += (s, m) => EnqueueMessage(m);
_webSocket.Connect();
new Thread(new ThreadStart(MessagesProcessorThread)).Start();
}
private void MessagesProcessorThread()
{
while (!_cts.IsCancellationRequested)
{
// wait for the messages
_processingEvent.WaitOne();
// process messages until there is at least one on the Q
while (!_cts.IsCancellationRequested)
{
// get the message if possible
WebSocketMessage msg = null;
lock (_processingQueue)
{
if (_processingQueue.Count > 0)
{
msg = _processingQueue.Dequeue();
}
}
if (msg == null)
{
// no more messages
break;
}
// process message
try
{
OnMessage(this, msg);
}
catch (Exception exception)
{
Log.Error($"Error processing message: {msg.Message} - Error: {exception}");
}
}
}
}
private void EnqueueMessage(WebSocketMessage msg)
{
lock (_processingQueue)
{
_processingQueue.Enqueue(msg);
}
_processingEvent.Set();
}
/// <summary>
/// Get the next ticks from the live trading data queue
/// </summary>
/// <returns>IEnumerable list of ticks since the last update.</returns>
public IEnumerable<BaseData> GetNextTicks()
{
lock (_locker)
{
var copy = _ticks.ToArray();
_ticks.Clear();
return copy;
}
}
/// <summary>
/// Adds the specified symbols to the subscription
/// </summary>
/// <param name="job">Job we're subscribing for:</param>
/// <param name="symbols">The symbols to be added keyed by SecurityType</param>
public void Subscribe(LiveNodePacket job, IEnumerable<Symbol> symbols)
{
lock (_lockerSubscriptions)
{
var symbolsToSubscribe = (from symbol in symbols
where !_subscribedSymbols.Contains(symbol) && CanSubscribe(symbol)
select symbol).ToList();
if (symbolsToSubscribe.Count == 0)
return;
Log.Trace($"CoinApiDataQueueHandler.Subscribe(): {string.Join(",", symbolsToSubscribe.Select(x => x.Value))}");
// CoinAPI requires at least 5 seconds between subscription requests so we need to batch them
_subscribedSymbols = symbolsToSubscribe.Concat(_subscribedSymbols).ToHashSet();
ProcessSubscriptionRequest();
}
}
/// <summary>
/// Removes the specified symbols to the subscription
/// </summary>
/// <param name="job">Job we're processing.</param>
/// <param name="symbols">The symbols to be removed keyed by SecurityType</param>
public void Unsubscribe(LiveNodePacket job, IEnumerable<Symbol> symbols)
{
lock (_lockerSubscriptions)
{
var symbolsToUnsubscribe = (from symbol in symbols
where _subscribedSymbols.Contains(symbol)
select symbol).ToList();
if (symbolsToUnsubscribe.Count == 0)
return;
Log.Trace($"CoinApiDataQueueHandler.Unsubscribe(): {string.Join(",", symbolsToUnsubscribe.Select(x => x.Value))}");
// CoinAPI requires at least 5 seconds between subscription requests so we need to batch them
_subscribedSymbols = _subscribedSymbols.Where(x => !symbolsToUnsubscribe.Contains(x)).ToHashSet();
ProcessSubscriptionRequest();
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
_connectionHandler.DisposeSafely();
if (_webSocket.IsOpen)
{
_webSocket.Close();
}
}
/// <summary>
/// Helper method used in QC backend
/// </summary>
/// <param name="markets">List of LEAN markets (exchanges) to subscribe</param>
public void SubscribeMarkets(List<string> markets)
{
Log.Trace($"CoinApiDataQueueHandler.SubscribeMarkets(): {string.Join(",", markets)}");
_subscribedExchanges = markets.ToList();
SendHelloMessage(markets.Select(x => _symbolMapper.GetExchangeId(x)));
_connectionHandler.EnableMonitoring(true);
}
private void ProcessSubscriptionRequest()
{
if (_subscriptionsPending) return;
_lastSubscribeRequestUtcTime = DateTime.UtcNow;
_subscriptionsPending = true;
Task.Run(async () =>
{
while (true)
{
DateTime requestTime;
List<Symbol> symbolsToSubscribe;
lock (_lockerSubscriptions)
{
requestTime = _lastSubscribeRequestUtcTime.Add(_subscribeDelay);
// CoinAPI requires at least 5 seconds between hello messages
if (_nextHelloMessageUtcTime != DateTime.MinValue && requestTime < _nextHelloMessageUtcTime)
{
requestTime = _nextHelloMessageUtcTime;
}
symbolsToSubscribe = _subscribedSymbols.ToList();
}
var timeToWait = requestTime - DateTime.UtcNow;
int delayMilliseconds;
if (timeToWait <= TimeSpan.Zero)
{
// minimum delay has passed since last subscribe request, send the Hello message
SubscribeSymbols(symbolsToSubscribe);
lock (_lockerSubscriptions)
{
_lastSubscribeRequestUtcTime = DateTime.UtcNow;
if (_subscribedSymbols.Count == symbolsToSubscribe.Count)
{
// no more subscriptions pending, task finished
_subscriptionsPending = false;
break;
}
}
delayMilliseconds = _subscribeDelay.Milliseconds;
}
else
{
delayMilliseconds = timeToWait.Milliseconds;
}
await Task.Delay(delayMilliseconds).ConfigureAwait(false);
}
});
}
/// <summary>
/// Returns true if we can subscribe to the specified symbol
/// </summary>
private static bool CanSubscribe(Symbol symbol)
{
// ignore unsupported security types
if (symbol.ID.SecurityType != SecurityType.Crypto)
return false;
// ignore universe symbols
return !symbol.Value.Contains("-UNIVERSE-");
}
/// <summary>
/// Subscribes to a list of symbols
/// </summary>
/// <param name="symbolsToSubscribe">The list of symbols to subscribe</param>
private void SubscribeSymbols(List<Symbol> symbolsToSubscribe)
{
Log.Trace($"CoinApiDataQueueHandler.SubscribeSymbols(): {string.Join(",", symbolsToSubscribe)}");
SendHelloMessage(_subscribedSymbols.Select(_symbolMapper.GetBrokerageSymbol));
_connectionHandler.EnableMonitoring(true);
}
private void SendHelloMessage(IEnumerable<string> subscribeFilter)
{
var list = subscribeFilter.ToList();
if (list.Count == 0)
{
// If we use a null or empty filter in the CoinAPI hello message
// we will be subscribing to all symbols for all active exchanges!
// Only option is requesting an invalid symbol as filter.
list.Add("$no_symbol_requested$");
}
var message = JsonConvert.SerializeObject(new HelloMessage
{
ApiKey = _apiKey,
Heartbeat = true,
SubscribeDataType = new[] { "trade", "quote" },
SubscribeFilterSymbolId = list.ToArray()
});
_webSocket.Send(message);
_nextHelloMessageUtcTime = DateTime.UtcNow.Add(_minimumTimeBetweenHelloMessages);
}
private void OnMessage(object sender, WebSocketMessage e)
{
var jObject = JObject.Parse(e.Message);
var type = jObject["type"].ToString();
switch (type)
{
case "trade":
{
var trade = jObject.ToObject<TradeMessage>();
var item = new Tick
{
Symbol = _symbolMapper.GetLeanSymbol(trade.SymbolId, SecurityType.Crypto, string.Empty),
Time = trade.TimeExchange,
Value = trade.Price,
Quantity = trade.Size,
TickType = TickType.Trade
};
lock (_locker)
{
_ticks.Add(item);
}
_connectionHandler.KeepAlive(DateTime.UtcNow);
break;
}
case "quote":
{
var quote = jObject.ToObject<QuoteMessage>();
var tick = new Tick
{
Symbol = _symbolMapper.GetLeanSymbol(quote.SymbolId, SecurityType.Crypto, string.Empty),
Time = quote.TimeExchange,
AskPrice = quote.AskPrice,
AskSize = quote.AskSize,
BidPrice = quote.BidPrice,
BidSize = quote.BidSize,
TickType = TickType.Quote
};
lock (_locker)
{
// only emit quote ticks if bid price or ask price changed
Tick previousQuote;
if (!_previousQuotes.TryGetValue(tick.Symbol, out previousQuote) ||
tick.AskPrice != previousQuote.AskPrice ||
tick.BidPrice != previousQuote.BidPrice)
{
_previousQuotes[tick.Symbol] = tick;
_ticks.Add(tick);
}
}
_connectionHandler.KeepAlive(DateTime.UtcNow);
break;
}
// not a typo :)
case "hearbeat":
// just in case the typo will be fixed in the future
case "heartbeat":
_connectionHandler.KeepAlive(DateTime.UtcNow);
break;
case "error":
{
var error = jObject.ToObject<ErrorMessage>();
Log.Error(error.Message);
break;
}
default:
Log.Trace(e.Message);
break;
}
}
private void OnConnectionLost(object sender, EventArgs e)
{
Log.Error("CoinApiDataQueueHandler.OnConnectionLost(): CoinAPI connection lost.");
}
private void OnConnectionRestored(object sender, EventArgs e)
{
Log.Trace("CoinApiDataQueueHandler.OnConnectionRestored(): CoinAPI connection restored.");
}
private void OnReconnectRequested(object sender, EventArgs e)
{
Log.Trace($"CoinApiDataQueueHandler.OnReconnectRequested(): CoinAPI reconnection requested: IsOpen:{_webSocket.IsOpen} ReadyState:{_webSocket.ReadyState}");
if (!_webSocket.IsOpen)
{
Log.Trace("CoinApiDataQueueHandler.OnReconnectRequested(): Websocket connecting.");
_webSocket.Connect();
}
if (!_webSocket.IsOpen)
{
Log.Trace($"CoinApiDataQueueHandler.OnReconnectRequested(): Websocket not open: IsOpen:{_webSocket.IsOpen} ReadyState:{_webSocket.ReadyState}");
return;
}
Log.Trace($"CoinApiDataQueueHandler.OnReconnectRequested(): Reconnected: IsOpen:{_webSocket.IsOpen} ReadyState:{_webSocket.ReadyState}");
if (_subscribedExchanges.Count > 0)
{
Log.Trace("CoinApiDataQueueHandler.OnReconnectRequested(): Subscribe markets.");
SubscribeMarkets(_subscribedExchanges);
}
else if (_subscribedSymbols.Count > 0)
{
Log.Trace("CoinApiDataQueueHandler.OnReconnectRequested(): Subscribe symbols.");
SubscribeSymbols(_subscribedSymbols.ToList());
}
}
}
}