/* * 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.Net; using System.Threading; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NodaTime; using QuantConnect.Configuration; using QuantConnect.Data; using QuantConnect.Data.Market; using QuantConnect.Interfaces; using QuantConnect.Lean.Engine.DataFeeds; using QuantConnect.Lean.Engine.HistoricalData; using QuantConnect.Logging; using QuantConnect.Packets; using QuantConnect.Securities; using QuantConnect.Securities.Crypto; using QuantConnect.Securities.Forex; using QuantConnect.ToolBox.Polygon.WebSocket; using QuantConnect.ToolBox.Polygon.History; using QuantConnect.Util; using HistoryRequest = QuantConnect.Data.HistoryRequest; using static QuantConnect.StringExtensions; namespace QuantConnect.ToolBox.Polygon { /// /// An implementation of and for Polygon.io /// public class PolygonDataQueueHandler : SynchronizingHistoryProvider, IDataQueueHandler { private const string HistoryBaseUrl = "https://api.polygon.io"; private readonly string _apiKey = Config.Get("polygon-api-key"); private readonly IDataAggregator _dataAggregator = Composer.Instance.GetExportedValueByTypeName( Config.Get("data-aggregator", "QuantConnect.Lean.Engine.DataFeeds.AggregationManager")); private readonly DataQueueHandlerSubscriptionManager _subscriptionManager; private readonly Dictionary _webSocketClientWrappers = new Dictionary(); private readonly PolygonSymbolMapper _symbolMapper = new PolygonSymbolMapper(); private readonly MarketHoursDatabase _marketHoursDatabase = MarketHoursDatabase.FromDataFolder(); private readonly SymbolPropertiesDatabase _symbolPropertiesDatabase = SymbolPropertiesDatabase.FromDataFolder(); // exchange time zones by symbol private readonly Dictionary _symbolExchangeTimeZones = new Dictionary(); // map Polygon exchange -> Lean market // Crypto exchanges from: https://api.polygon.io/v1/meta/crypto-exchanges?apiKey=xxx private readonly Dictionary _cryptoExchangeMap = new Dictionary { { 1, Market.GDAX }, { 2, Market.Bitfinex } }; private int _dataPointCount; /// /// Static constructor for the class /// static PolygonDataQueueHandler() { // Polygon.io requires TLS 1.2 ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; } /// /// Initializes a new instance of the class /// public PolygonDataQueueHandler() : this(true) { } /// /// Initializes a new instance of the class /// public PolygonDataQueueHandler(bool streamingEnabled) { if (streamingEnabled) { foreach (var securityType in new[] { SecurityType.Equity, SecurityType.Forex, SecurityType.Crypto }) { var client = new PolygonWebSocketClientWrapper(_apiKey, _symbolMapper, securityType, OnMessage); _webSocketClientWrappers.Add(securityType, client); } } var subscriber = new EventBasedDataQueueHandlerSubscriptionManager(t => t.ToString()); subscriber.SubscribeImpl += Subscribe; subscriber.UnsubscribeImpl += Unsubscribe; _subscriptionManager = subscriber; } #region IDataQueueHandler implementation /// /// Sets the job we're subscribing for /// /// Job we're subscribing for public void SetJob(LiveNodePacket job) { } /// /// Indicates the connection is live. /// public bool IsConnected => _webSocketClientWrappers.Values.All(client => client.IsOpen); /// /// Subscribe to the specified configuration /// /// defines the parameters to subscribe to a data feed /// handler to be fired on new data available /// The new enumerator for this subscription request public IEnumerator Subscribe(SubscriptionDataConfig dataConfig, EventHandler newDataAvailableHandler) { if (!CanSubscribe(dataConfig.Symbol)) { return Enumerable.Empty().GetEnumerator(); } var enumerator = _dataAggregator.Add(dataConfig, newDataAvailableHandler); _subscriptionManager.Subscribe(dataConfig); return enumerator; } /// /// Removes the specified configuration /// /// Subscription config to be removed public void Unsubscribe(SubscriptionDataConfig dataConfig) { _subscriptionManager.Unsubscribe(dataConfig); _dataAggregator.Remove(dataConfig); } /// /// Adds the specified symbols to the subscription /// /// The symbols to be added /// Type of tick data private bool Subscribe(IEnumerable symbols, TickType tickType) { foreach (var symbol in symbols) { var webSocket = GetWebSocket(symbol.SecurityType); webSocket.Subscribe(symbol, tickType); } return true; } /// /// Removes the specified symbols from the subscription /// /// The symbols to be removed /// Type of tick data private bool Unsubscribe(IEnumerable symbols, TickType tickType) { foreach (var symbol in symbols) { var webSocket = GetWebSocket(symbol.SecurityType); webSocket.Unsubscribe(symbol, tickType); } return true; } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { } #endregion #region IHistoryProvider implementation /// /// Gets the total number of data points emitted by this history provider /// public override int DataPointCount => _dataPointCount; /// /// Initializes this history provider to work for the specified job /// /// The initialization parameters public override void Initialize(HistoryProviderInitializeParameters parameters) { } /// /// Gets the history for the requested securities /// /// The historical data requests /// The time zone used when time stamping the slice instances /// An enumerable of the slices of data covering the span specified in each request public override IEnumerable GetHistory(IEnumerable requests, DateTimeZone sliceTimeZone) { // create subscription objects from the configs var subscriptions = new List(); foreach (var request in requests) { var history = GetHistory(request); var subscription = CreateSubscription(request, history); subscriptions.Add(subscription); } return CreateSliceEnumerableFromSubscriptions(subscriptions, sliceTimeZone); } /// /// Gets the history for the requested security /// /// The historical data request /// An enumerable of BaseData points public IEnumerable GetHistory(HistoryRequest request) { return ProcessHistoryRequest(request); } #endregion private IEnumerable ProcessHistoryRequest(HistoryRequest request) { if (string.IsNullOrWhiteSpace(_apiKey)) { Log.Error("PolygonDataQueueHandler.GetHistory(): History calls for Polygon.io require an API key."); yield break; } // check security type if (request.Symbol.SecurityType != SecurityType.Equity && request.Symbol.SecurityType != SecurityType.Forex && request.Symbol.SecurityType != SecurityType.Crypto) { Log.Error($"PolygonDataQueueHandler.ProcessHistoryRequests(): Unsupported security type: {request.Symbol.SecurityType}."); yield break; } // check tick type if (request.TickType != TickType.Trade && request.TickType != TickType.Quote) { Log.Error($"PolygonDataQueueHandler.ProcessHistoryRequests(): Unsupported tick type: {request.TickType}."); yield break; } // check unsupported security type/tick type combinations if (request.Symbol.SecurityType == SecurityType.Forex && request.TickType != TickType.Quote || request.Symbol.SecurityType == SecurityType.Crypto && request.TickType != TickType.Trade) { Log.Error($"PolygonDataQueueHandler.ProcessHistoryRequests(): Unsupported history request: {request.Symbol.SecurityType}/{request.TickType}."); yield break; } Log.Trace("PolygonDataQueueHandler.ProcessHistoryRequests(): Submitting request: " + Invariant($"{request.Symbol.SecurityType}-{request.TickType}-{request.Symbol.Value}: {request.Resolution} {request.StartTimeUtc}->{request.EndTimeUtc}")); switch (request.Resolution) { case Resolution.Tick: if (request.TickType == TickType.Trade) { foreach (var tick in GetTradeTicks(request)) { Interlocked.Increment(ref _dataPointCount); yield return tick; } } else if (request.TickType == TickType.Quote) { foreach (var tick in GetQuoteTicks(request)) { Interlocked.Increment(ref _dataPointCount); yield return tick; } } break; case Resolution.Second: if (request.TickType == TickType.Trade) { var ticks = GetTradeTicks(request); foreach (var tradeBar in AggregateTradeTicks(request.Symbol, ticks, request.Resolution.ToTimeSpan())) { Interlocked.Increment(ref _dataPointCount); yield return tradeBar; } } else if (request.TickType == TickType.Quote) { var ticks = GetQuoteTicks(request); foreach (var quoteBar in AggregateQuoteTicks(request.Symbol, ticks, request.Resolution.ToTimeSpan())) { Interlocked.Increment(ref _dataPointCount); yield return quoteBar; } } break; case Resolution.Minute: case Resolution.Hour: case Resolution.Daily: if (request.TickType == TickType.Trade) { foreach (var tradeBar in GetTradeBars(request)) { Interlocked.Increment(ref _dataPointCount); yield return tradeBar; } } else if (request.TickType == TickType.Quote) { var ticks = GetQuoteTicks(request); foreach (var quoteBar in AggregateQuoteTicks(request.Symbol, ticks, request.Resolution.ToTimeSpan())) { Interlocked.Increment(ref _dataPointCount); yield return quoteBar; } } break; } } private IEnumerable GetQuoteTicks(HistoryRequest request) { switch (request.Symbol.SecurityType) { case SecurityType.Equity: return GetEquityQuoteTicks(request); case SecurityType.Forex: return GetForexQuoteTicks(request); default: return Enumerable.Empty(); } } private IEnumerable GetTradeTicks(HistoryRequest request) { switch (request.Symbol.SecurityType) { case SecurityType.Equity: return GetEquityTradeTicks(request); case SecurityType.Crypto: return GetCryptoTradeTicks(request); default: return Enumerable.Empty(); } } private IEnumerable GetForexQuoteTicks(HistoryRequest request) { // https://api.polygon.io/v1/historic/forex/EUR/USD/2020-08-24?apiKey= var start = request.StartTimeUtc; var end = request.EndTimeUtc; while (start <= end) { using (var client = new WebClient()) { string baseCurrency; string quoteCurrency; Forex.DecomposeCurrencyPair(request.Symbol.Value, out baseCurrency, out quoteCurrency); var offset = Convert.ToInt64(Time.DateTimeToUnixTimeStampMilliseconds(start)); var url = $"{HistoryBaseUrl}/v1/historic/forex/{baseCurrency}/{quoteCurrency}/{start.Date:yyyy-MM-dd}?apiKey={_apiKey}&offset={offset}"; var response = client.DownloadString(url); var obj = JObject.Parse(response); var objTicks = obj["ticks"]; if (objTicks.Type == JTokenType.Null) { // current date finished, move to next day start = start.Date.AddDays(1); continue; } foreach (var objTick in objTicks) { var row = objTick.ToObject(); var utcTime = Time.UnixMillisecondTimeStampToDateTime(row.Timestamp); if (utcTime < start) { continue; } start = utcTime.AddMilliseconds(1); if (utcTime > end) { yield break; } var time = GetTickTime(request.Symbol, utcTime); yield return new Tick(time, request.Symbol, row.Bid, row.Ask); } } } } private IEnumerable GetCryptoTradeTicks(HistoryRequest request) { // https://api.polygon.io/v1/historic/crypto/BTC/USD/2020-08-24?apiKey= var start = request.StartTimeUtc; var end = request.EndTimeUtc; while (start <= end) { using (var client = new WebClient()) { var symbolProperties = _symbolPropertiesDatabase.GetSymbolProperties( request.Symbol.ID.Market, request.Symbol, request.Symbol.SecurityType, Currencies.USD); string baseCurrency; string quoteCurrency; Crypto.DecomposeCurrencyPair(request.Symbol, symbolProperties, out baseCurrency, out quoteCurrency); var offset = Convert.ToInt64(Time.DateTimeToUnixTimeStampMilliseconds(start)); var url = $"{HistoryBaseUrl}/v1/historic/crypto/{baseCurrency}/{quoteCurrency}/{start.Date:yyyy-MM-dd}?apiKey={_apiKey}&offset={offset}"; var response = client.DownloadString(url); var obj = JObject.Parse(response); var objTicks = obj["ticks"]; if (objTicks.Type == JTokenType.Null) { // current date finished, move to next day start = start.Date.AddDays(1); continue; } foreach (var objTick in objTicks) { var row = objTick.ToObject(); var utcTime = Time.UnixMillisecondTimeStampToDateTime(row.Timestamp); if (utcTime < start) { continue; } start = utcTime.AddMilliseconds(1); if (utcTime > end) { yield break; } var market = GetMarketFromCryptoExchangeId(row.Exchange); if (market != request.Symbol.ID.Market) { continue; } var time = GetTickTime(request.Symbol, utcTime); yield return new Tick(time, request.Symbol, string.Empty, string.Empty, row.Size, row.Price); } } } } private IEnumerable GetEquityQuoteTicks(HistoryRequest request) { // https://api.polygon.io/v2/ticks/stocks/nbbo/SPY/2020-08-24?apiKey= var start = request.StartTimeUtc; var end = request.EndTimeUtc; while (start <= end) { using (var client = new WebClient()) { var offset = Time.DateTimeToUnixTimeStampNanoseconds(start); var url = $"{HistoryBaseUrl}/v2/ticks/stocks/nbbo/{request.Symbol.Value}/{start.Date:yyyy-MM-dd}?apiKey={_apiKey}×tamp={offset}"; var response = client.DownloadString(url); var obj = JObject.Parse(response); var objTicks = obj["results"]; if (objTicks.Type == JTokenType.Null || !objTicks.Any()) { // current date finished, move to next day start = start.Date.AddDays(1); continue; } foreach (var objTick in objTicks) { var row = objTick.ToObject(); var utcTime = Time.UnixNanosecondTimeStampToDateTime(row.ExchangeTimestamp); if (utcTime < start) { continue; } start = utcTime.AddMilliseconds(1); if (utcTime > end) { yield break; } var time = GetTickTime(request.Symbol, utcTime); yield return new Tick(time, request.Symbol, string.Empty, string.Empty, row.BidSize, row.BidPrice, row.AskSize, row.AskPrice); } } } } private IEnumerable GetEquityTradeTicks(HistoryRequest request) { // https://api.polygon.io/v2/ticks/stocks/trades/SPY/2020-08-24?apiKey= var start = request.StartTimeUtc; var end = request.EndTimeUtc; while (start <= end) { using (var client = new WebClient()) { var offset = Time.DateTimeToUnixTimeStampNanoseconds(start); var url = $"{HistoryBaseUrl}/v2/ticks/stocks/trades/{request.Symbol.Value}/{start.Date:yyyy-MM-dd}?apiKey={_apiKey}×tamp={offset}"; var response = client.DownloadString(url); var obj = JObject.Parse(response); var objTicks = obj["results"]; if (objTicks.Type == JTokenType.Null || !objTicks.Any()) { // current date finished, move to next day start = start.Date.AddDays(1); continue; } foreach (var objTick in objTicks) { var row = objTick.ToObject(); var utcTime = Time.UnixNanosecondTimeStampToDateTime(row.ExchangeTimestamp); if (utcTime < start) { continue; } start = utcTime.AddMilliseconds(1); if (utcTime > end) { yield break; } var time = GetTickTime(request.Symbol, utcTime); yield return new Tick(time, request.Symbol, string.Empty, string.Empty, row.Size, row.Price); } } } } private IEnumerable GetTradeBars(HistoryRequest request) { var historyTimespan = GetHistoryTimespan(request.Resolution); string tickerPrefix; switch (request.Symbol.SecurityType) { case SecurityType.Forex: tickerPrefix = "C:"; break; case SecurityType.Crypto: tickerPrefix = "X:"; break; default: tickerPrefix = string.Empty; break; } var start = request.StartTimeUtc; var end = request.EndTimeUtc; using (var client = new WebClient()) { var url = $"{HistoryBaseUrl}/v2/aggs/ticker/{tickerPrefix}{request.Symbol.Value}/range/1/{historyTimespan}/{start.Date:yyyy-MM-dd}/{end.Date:yyyy-MM-dd}?apiKey={_apiKey}"; var response = client.DownloadString(url); var result = JsonConvert.DeserializeObject(response); if (result.Results == null) { yield break; } foreach (var row in result.Results) { var utcTime = Time.UnixMillisecondTimeStampToDateTime(row.Timestamp); if (utcTime < request.StartTimeUtc) { continue; } if (utcTime > request.EndTimeUtc.Add(request.Resolution.ToTimeSpan())) { yield break; } var time = GetTickTime(request.Symbol, utcTime); yield return new TradeBar(time, request.Symbol, row.Open, row.High, row.Low, row.Close, row.Volume); } } } private static IEnumerable AggregateTradeTicks(Symbol symbol, IEnumerable ticks, TimeSpan period) { return from t in ticks group t by t.Time.RoundDown(period) into g select new TradeBar { Symbol = symbol, Time = g.Key, Open = g.First().LastPrice, High = g.Max(t => t.LastPrice), Low = g.Min(t => t.LastPrice), Close = g.Last().LastPrice, Volume = g.Sum(t => t.Quantity), Period = period }; } private static IEnumerable AggregateQuoteTicks(Symbol symbol, IEnumerable ticks, TimeSpan period) { return from t in ticks group t by t.Time.RoundDown(period) into g select new QuoteBar { Symbol = symbol, Time = g.Key, Bid = new Bar { Open = g.First().BidPrice, High = g.Max(b => b.BidPrice), Low = g.Min(b => b.BidPrice), Close = g.Last().BidPrice }, Ask = new Bar { Open = g.First().AskPrice, High = g.Max(b => b.AskPrice), Low = g.Min(b => b.AskPrice), Close = g.Last().AskPrice }, Period = period }; } private static string GetHistoryTimespan(Resolution resolution) { switch (resolution) { case Resolution.Daily: return "day"; case Resolution.Hour: return "hour"; case Resolution.Minute: return "minute"; default: throw new Exception($"PolygonDataQueueHandler.GetHistoryTimespan(): unsupported resolution: {resolution}."); } } private PolygonWebSocketClientWrapper GetWebSocket(SecurityType securityType) { PolygonWebSocketClientWrapper client; if (!_webSocketClientWrappers.TryGetValue(securityType, out client)) { throw new Exception($"Unsupported security type: {securityType}"); } return client; } private static bool CanSubscribe(Symbol symbol) { var securityType = symbol.ID.SecurityType; if (symbol.Value.IndexOfInvariant("universe", true) != -1) return false; return securityType == SecurityType.Equity || securityType == SecurityType.Forex || securityType == SecurityType.Crypto; } private void OnMessage(string message) { foreach (var obj in JArray.Parse(message)) { var eventType = obj["ev"].ToString(); switch (eventType) { case "T": ProcessEquityTrade(obj.ToObject()); break; case "Q": ProcessEquityQuote(obj.ToObject()); break; case "C": ProcessForexQuote(obj.ToObject()); break; case "XT": ProcessCryptoTrade(obj.ToObject()); break; case "XQ": ProcessCryptoQuote(obj.ToObject()); break; } } } private void ProcessEquityTrade(EquityTradeMessage trade) { var symbol = _symbolMapper.GetLeanSymbol(trade.Symbol, SecurityType.Equity, Market.USA); var time = GetTickTime(symbol, trade.Timestamp); var tick = new Tick { TickType = TickType.Trade, Symbol = symbol, Time = time, Value = trade.Price, Quantity = trade.Size }; _dataAggregator.Update(tick); } private void ProcessEquityQuote(EquityQuoteMessage quote) { var symbol = _symbolMapper.GetLeanSymbol(quote.Symbol, SecurityType.Equity, Market.USA); var time = GetTickTime(symbol, quote.Timestamp); var tick = new Tick { TickType = TickType.Quote, Symbol = symbol, Time = time, AskPrice = quote.AskPrice, BidPrice = quote.BidPrice, AskSize = quote.AskSize, BidSize = quote.BidSize, Value = (quote.AskPrice + quote.BidPrice) / 2m }; _dataAggregator.Update(tick); } private void ProcessForexQuote(ForexQuoteMessage quote) { var symbol = _symbolMapper.GetLeanSymbol(quote.Symbol, SecurityType.Forex, Market.FXCM); var time = GetTickTime(symbol, quote.Timestamp); var tick = new Tick { TickType = TickType.Quote, Symbol = symbol, Time = time, AskPrice = quote.AskPrice, BidPrice = quote.BidPrice, Value = (quote.AskPrice + quote.BidPrice) / 2m }; _dataAggregator.Update(tick); } private void ProcessCryptoTrade(CryptoTradeMessage trade) { var market = GetMarketFromCryptoExchangeId(trade.ExchangeId); if (string.IsNullOrWhiteSpace(market)) { return; } var symbol = _symbolMapper.GetLeanSymbol(trade.Symbol, SecurityType.Crypto, market); var time = GetTickTime(symbol, trade.Timestamp); var tick = new Tick { TickType = TickType.Trade, Symbol = symbol, Time = time, Value = trade.Price, Quantity = trade.Size }; _dataAggregator.Update(tick); } private void ProcessCryptoQuote(CryptoQuoteMessage quote) { var market = GetMarketFromCryptoExchangeId(quote.ExchangeId); if (string.IsNullOrWhiteSpace(market)) { return; } var symbol = _symbolMapper.GetLeanSymbol(quote.Symbol, SecurityType.Crypto, market); var time = GetTickTime(symbol, quote.Timestamp); var tick = new Tick { TickType = TickType.Quote, Symbol = symbol, Time = time, AskPrice = quote.AskPrice, BidPrice = quote.BidPrice, AskSize = quote.AskSize, BidSize = quote.BidSize, Value = (quote.AskPrice + quote.BidPrice) / 2m }; _dataAggregator.Update(tick); } private DateTime GetTickTime(Symbol symbol, long timestamp) { var utcTime = Time.UnixMillisecondTimeStampToDateTime(timestamp); return GetTickTime(symbol, utcTime); } private DateTime GetTickTime(Symbol symbol, DateTime utcTime) { DateTimeZone exchangeTimeZone; if (!_symbolExchangeTimeZones.TryGetValue(symbol, out exchangeTimeZone)) { // read the exchange time zone from market-hours-database exchangeTimeZone = _marketHoursDatabase.GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType).TimeZone; _symbolExchangeTimeZones.Add(symbol, exchangeTimeZone); } return utcTime.ConvertFromUtc(exchangeTimeZone); } private string GetMarketFromCryptoExchangeId(int exchangeId) { string market; return _cryptoExchangeMap.TryGetValue(exchangeId, out market) ? market : string.Empty; } } }