75eac27795
* bitfinex unsubscribe impl * IB implementation * Alpaca Brokerage * Fxcm Brokerage * Bitfinex remake * GDAX Brokerage * move & rename DataQueueHandler subscription manager to better place for using in ToolBox * performance tuning use unified Channel class everywhere BaseWebsocketBrokerage.GetSubscribed method relies on DQHSubscriptionManager * TradierBrokerage implementation * OandaBrokerage implementation * FakeDataQueue subcription manager * Fake SubscriptionManager for testing * CoinApi implementation * IEXDataQueueHandler implementation * IQFeed Implementation * unit tests * fix SubscribeSymbols params * thread safe subscribed symbols * don't need to lock Keys prop, because it's thread safe * accurate PolygonDataQueueHandler subscribe/unsubscribe methods * Alpaca isnt DataQueueHAndler anymore * remove unused variable * remove redundant hashset * fix coin api subscribe method * disclaimer * prettify code * race condition? * fix symbol conversion * log if unsubscribed; change Channel Id type * requested changes * pass GetChannelName func as required parameter * centralized logs * use single name for all idqh with no difference in tick type * change Oanda resubscribe method * CanSubscribe doesn't change instance state - can be marked as static. * change Oanda Subscription tracking * style changes * bitfinex fix * fix spelling
323 lines
12 KiB
C#
323 lines
12 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.Tasks;
|
|
using CoinAPI.WebSocket.V1;
|
|
using CoinAPI.WebSocket.V1.DataModels;
|
|
using QuantConnect.Configuration;
|
|
using QuantConnect.Data;
|
|
using QuantConnect.Data.Market;
|
|
using QuantConnect.Interfaces;
|
|
using QuantConnect.Logging;
|
|
using QuantConnect.Packets;
|
|
using QuantConnect.Util;
|
|
|
|
namespace QuantConnect.ToolBox.CoinApi
|
|
{
|
|
/// <summary>
|
|
/// An implementation of <see cref="IDataQueueHandler"/> for CoinAPI
|
|
/// </summary>
|
|
public class CoinApiDataQueueHandler : IDataQueueHandler
|
|
{
|
|
private readonly string _apiKey = Config.Get("coinapi-api-key");
|
|
private readonly CoinApiWsClient _client;
|
|
private readonly object _locker = new object();
|
|
private readonly CoinApiSymbolMapper _symbolMapper = new CoinApiSymbolMapper();
|
|
private readonly IDataAggregator _dataAggregator = Composer.Instance.GetExportedValueByTypeName<IDataAggregator>(
|
|
Config.Get("data-aggregator", "QuantConnect.Lean.Engine.DataFeeds.AggregationManager"));
|
|
private readonly EventBasedDataQueueHandlerSubscriptionManager _subscriptionManager;
|
|
|
|
private readonly TimeSpan _subscribeDelay = TimeSpan.FromMilliseconds(250);
|
|
private readonly object _lockerSubscriptions = new object();
|
|
private DateTime _lastSubscribeRequestUtcTime = DateTime.MinValue;
|
|
private bool _subscriptionsPending;
|
|
|
|
private readonly TimeSpan _minimumTimeBetweenHelloMessages = TimeSpan.FromSeconds(5);
|
|
private DateTime _nextHelloMessageUtcTime = DateTime.MinValue;
|
|
|
|
private readonly Dictionary<Symbol, Tick> _previousQuotes = new Dictionary<Symbol, Tick>();
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="CoinApiDataQueueHandler"/> class
|
|
/// </summary>
|
|
public CoinApiDataQueueHandler()
|
|
{
|
|
_client = new CoinApiWsClient();
|
|
_client.TradeEvent += OnTrade;
|
|
_client.QuoteEvent += OnQuote;
|
|
_client.Error += OnError;
|
|
_subscriptionManager = new EventBasedDataQueueHandlerSubscriptionManager();
|
|
_subscriptionManager.SubscribeImpl += (s, t) => Subscribe(s);
|
|
_subscriptionManager.UnsubscribeImpl += (s, t) => Unsubscribe(s);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribe to the specified configuration
|
|
/// </summary>
|
|
/// <param name="dataConfig">defines the parameters to subscribe to a data feed</param>
|
|
/// <param name="newDataAvailableHandler">handler to be fired on new data available</param>
|
|
/// <returns>The new enumerator for this subscription request</returns>
|
|
public IEnumerator<BaseData> Subscribe(SubscriptionDataConfig dataConfig, EventHandler newDataAvailableHandler)
|
|
{
|
|
if (!CanSubscribe(dataConfig.Symbol))
|
|
{
|
|
return Enumerable.Empty<BaseData>().GetEnumerator();
|
|
}
|
|
|
|
var enumerator = _dataAggregator.Add(dataConfig, newDataAvailableHandler);
|
|
_subscriptionManager.Subscribe(dataConfig);
|
|
|
|
return enumerator;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the job we're subscribing for
|
|
/// </summary>
|
|
/// <param name="job">Job we're subscribing for</param>
|
|
public void SetJob(LiveNodePacket job)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the specified symbols to the subscription
|
|
/// </summary>
|
|
/// <param name="symbols">The symbols to be added keyed by SecurityType</param>
|
|
private bool Subscribe(IEnumerable<Symbol> symbols)
|
|
{
|
|
ProcessSubscriptionRequest();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the specified configuration
|
|
/// </summary>
|
|
/// <param name="dataConfig">Subscription config to be removed</param>
|
|
public void Unsubscribe(SubscriptionDataConfig dataConfig)
|
|
{
|
|
_subscriptionManager.Unsubscribe(dataConfig);
|
|
_dataAggregator.Remove(dataConfig);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Removes the specified symbols to the subscription
|
|
/// </summary>
|
|
/// <param name="symbols">The symbols to be removed keyed by SecurityType</param>
|
|
private bool Unsubscribe(IEnumerable<Symbol> symbols)
|
|
{
|
|
ProcessSubscriptionRequest();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether the data provider is connected
|
|
/// </summary>
|
|
/// <returns>true if the data provider is connected</returns>
|
|
public bool IsConnected => true;
|
|
|
|
/// <summary>
|
|
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
_client.TradeEvent -= OnTrade;
|
|
_client.QuoteEvent -= OnQuote;
|
|
_client.Error -= OnError;
|
|
_client.Dispose();
|
|
_dataAggregator.DisposeSafely();
|
|
}
|
|
|
|
/// <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)}");
|
|
|
|
SendHelloMessage(markets.Select(x => _symbolMapper.GetExchangeId(x)));
|
|
}
|
|
|
|
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 = _subscriptionManager.GetSubscribedSymbols().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 (_subscriptionManager.GetSubscribedSymbols().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(symbolsToSubscribe.Select(_symbolMapper.GetBrokerageSymbol));
|
|
}
|
|
|
|
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$");
|
|
}
|
|
|
|
_client.SendHelloMessage(new Hello
|
|
{
|
|
apikey = Guid.Parse(_apiKey),
|
|
heartbeat = true,
|
|
subscribe_data_type = new[] { "trade", "quote" },
|
|
subscribe_filter_symbol_id = list.ToArray()
|
|
});
|
|
|
|
_nextHelloMessageUtcTime = DateTime.UtcNow.Add(_minimumTimeBetweenHelloMessages);
|
|
}
|
|
|
|
private void OnTrade(object sender, Trade trade)
|
|
{
|
|
try
|
|
{
|
|
var tick = new Tick
|
|
{
|
|
Symbol = _symbolMapper.GetLeanSymbol(trade.symbol_id, SecurityType.Crypto, string.Empty),
|
|
Time = trade.time_exchange,
|
|
Value = trade.price,
|
|
Quantity = trade.size,
|
|
TickType = TickType.Trade
|
|
};
|
|
|
|
lock (_locker)
|
|
{
|
|
_dataAggregator.Update(tick);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error(e);
|
|
}
|
|
}
|
|
|
|
private void OnQuote(object sender, Quote quote)
|
|
{
|
|
try
|
|
{
|
|
var tick = new Tick
|
|
{
|
|
Symbol = _symbolMapper.GetLeanSymbol(quote.symbol_id, SecurityType.Crypto, string.Empty),
|
|
Time = quote.time_exchange,
|
|
AskPrice = quote.ask_price,
|
|
AskSize = quote.ask_size,
|
|
BidPrice = quote.bid_price,
|
|
BidSize = quote.bid_size,
|
|
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;
|
|
_dataAggregator.Update(tick);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error(e);
|
|
}
|
|
}
|
|
|
|
private void OnError(object sender, Exception e)
|
|
{
|
|
Log.Error(e);
|
|
}
|
|
}
|
|
}
|