/* * 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 QuantConnect.Data; using QuantConnect.Packets; using QuantConnect.Configuration; using Quobject.SocketIoClientDotNet.Client; using QuantConnect.Logging; using Newtonsoft.Json.Linq; using QuantConnect.Data.Market; using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using System.Text; using QuantConnect.Interfaces; using NodaTime; using System.Globalization; namespace QuantConnect.ToolBox.IEX { /// /// IEX live data handler. /// Data provided for free by IEX. See more at https://iextrading.com/api-exhibit-a /// public class IEXDataQueueHandler : HistoryProviderBase, IDataQueueHandler, IDisposable { // using SocketIoClientDotNet is a temp solution until IEX implements standard WebSockets protocol private Socket _socket; private readonly string _apiKey; private ConcurrentDictionary _symbols = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); private Manager _manager; private CancellationTokenSource _cts = new CancellationTokenSource(); private static DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Unspecified); private TaskCompletionSource _connected = new TaskCompletionSource(); private Task _lastEmitTask; private bool _subscribedToAll; private int _dataPointCount; private BlockingCollection _outputCollection = new BlockingCollection(); public string Endpoint { get; internal set; } public bool IsConnected { get { return _manager.ReadyState == Manager.ReadyStateEnum.OPEN; } } public IEXDataQueueHandler(bool live = true, string apiKey = null) { Endpoint = "https://ws-api.iextrading.com/1.0/tops"; if (live) Reconnect(); _apiKey = apiKey; } internal void Reconnect() { try { _socket = IO.Socket(Endpoint, new IO.Options() { // default is 1000, default attempts is int.MaxValue ReconnectionDelay = 1000 }); _socket.On(Socket.EVENT_CONNECT, () => { _connected.TrySetResult(true); Log.Trace("IEXDataQueueHandler.Reconnect(): Connected to IEX live data"); Log.Trace("IEXDataQueueHandler.Reconnect(): IEX Real-Time Price"); }); _socket.On("message", message => ProcessJsonObject(JObject.Parse((string)message))); _manager = _socket.Io(); } catch (Exception err) { Log.Error("IEXDataQueueHandler.Reconnect(): " + err.Message); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void ProcessJsonObject(JObject message) { try { // https://iextrading.com/developer/#tops-tops-response var symbolString = message["symbol"].Value(); Symbol symbol; if (!_symbols.TryGetValue(symbolString, out symbol)) { if (_subscribedToAll) { symbol = Symbol.Create(symbolString, SecurityType.Equity, Market.USA); } else { Log.Trace("IEXDataQueueHandler.ProcessJsonObject(): Received unexpected symbol '" + symbolString + "' from IEX in IEXDataQueueHandler"); return; } } var bidSize = message["bidSize"].Value(); var bidPrice = message["bidPrice"].Value(); var askSize = message["askSize"].Value(); var askPrice = message["askPrice"].Value(); var volume = message["volume"].Value(); var lastSalePrice = message["lastSalePrice"].Value(); var lastSaleSize = message["lastSaleSize"].Value(); var lastSaleTime = message["lastSaleTime"].Value(); var lastSaleDateTime = UnixEpoch.AddMilliseconds(lastSaleTime); var lastUpdated = message["lastUpdated"].Value(); if (lastUpdated == -1) { // there were no trades on this day return; } var lastUpdatedDatetime = UnixEpoch.AddMilliseconds(lastUpdated); var tick = new Tick() { Symbol = symbol, Time = lastUpdatedDatetime.ConvertFromUtc(TimeZones.NewYork), TickType = lastUpdatedDatetime == lastSaleDateTime ? TickType.Trade : TickType.Quote, Exchange = "IEX", BidSize = bidSize, BidPrice = bidPrice, AskSize = askSize, AskPrice = askPrice, Value = lastSalePrice, Quantity = lastSaleSize }; _outputCollection.TryAdd(tick); } catch (Exception err) { // this method should never fail Log.Error("IEXDataQueueHandler.ProcessJsonObject(): " + err.Message); } } /// /// Desktop/Local doesn't support live data from this handler /// /// Tick public IEnumerable GetNextTicks() { return _outputCollection.GetConsumingEnumerable(); } /// /// Subscribe to symbols /// public void Subscribe(LiveNodePacket job, IEnumerable symbols) { try { var sb = new StringBuilder(); foreach (var symbol in symbols) { // IEX only supports equities if (symbol.SecurityType != SecurityType.Equity) continue; if (symbol.Value.Equals("firehose", StringComparison.InvariantCultureIgnoreCase)) { _subscribedToAll = true; } if (_symbols.TryAdd(symbol.Value, symbol)) { // added new symbol sb.Append(symbol.Value); sb.Append(","); } } var symbolsList = sb.ToString().TrimEnd(','); if (!String.IsNullOrEmpty(symbolsList)) { SocketSafeAsyncEmit("subscribe", symbolsList); } } catch (Exception err) { Log.Error("IEXDataQueueHandler.Subscribe(): " + err.Message); } } /// /// Unsubscribe from symbols /// public void Unsubscribe(LiveNodePacket job, IEnumerable symbols) { try { var sb = new StringBuilder(); foreach (var symbol in symbols) { // IEX only supports equities if (symbol.SecurityType != SecurityType.Equity) continue; Symbol tmp; if (_symbols.TryRemove(symbol.Value, out tmp)) { // removed existing Trace.Assert(symbol.Value == tmp.Value); sb.Append(symbol.Value); sb.Append(","); } } var symbolsList = sb.ToString().TrimEnd(','); if (!String.IsNullOrEmpty(symbolsList)) { SocketSafeAsyncEmit("unsubscribe", symbolsList); } } catch (Exception err) { Log.Error("IEXDataQueueHandler.Unsubscribe(): " + err.Message); } } /// /// This method is used to schedule _socket.Emit request until the connection state is OPEN /// /// private void SocketSafeAsyncEmit(string command, string value) { Task.Run(async () => { await _connected.Task; const int retriesLimit = 100; var retriesCount = 0; while (true) { try { if (_manager.ReadyState == Manager.ReadyStateEnum.OPEN) { // there is an ACK functionality in socket.io, but IEX will be moving to standard WebSockets // and this retry logic is just for rare cases of connection interrupts _socket.Emit(command, value); break; } } catch (Exception err) { Log.Error("IEXDataQueueHandler.SocketSafeAsyncEmit(): " + err.Message); } await Task.Delay(100); retriesCount++; if (retriesCount >= retriesLimit) { Log.Error("IEXDataQueueHandler.SocketSafeAsyncEmit(): " + (new TimeoutException("Cannot subscribe to symbol :" + value))); break; } } }, _cts.Token) .ContinueWith((t) => { Log.Error("IEXDataQueueHandler.SocketSafeAsyncEmit(): " + t.Exception.Message); return t; }, TaskContinuationOptions.OnlyOnFaulted); } /// /// Dispose connection to IEX /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { _outputCollection.CompleteAdding(); _cts.Cancel(); if (_socket != null) { _socket.Disconnect(); _socket.Close(); } Log.Trace("IEXDataQueueHandler.Dispose(): Disconnected from IEX live data"); } ~IEXDataQueueHandler() { Dispose(false); } #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) { foreach (var request in requests) { foreach (var slice in ProcessHistoryRequests(request)) { yield return slice; } } } /// /// Populate request data /// private IEnumerable ProcessHistoryRequests(Data.HistoryRequest request) { var ticker = request.Symbol.ID.Symbol; var start = request.StartTimeUtc.ConvertFromUtc(TimeZones.NewYork); var end = request.EndTimeUtc.ConvertFromUtc(TimeZones.NewYork); if (request.Resolution == Resolution.Minute && start <= DateTime.Today.AddDays(-30)) { Log.Error("IEXDataQueueHandler.GetHistory(): History calls with minute resolution for IEX available only for trailing 30 calendar days."); yield break; } else if (request.Resolution != Resolution.Daily && request.Resolution != Resolution.Minute) { Log.Error("IEXDataQueueHandler.GetHistory(): History calls for IEX only support daily & minute resolution."); yield break; } if (start <= DateTime.Today.AddYears(-5)) { Log.Error("IEXDataQueueHandler.GetHistory(): History calls for IEX only support a maximum of 5 years history."); yield break; } Log.Trace(string.Format("IEXDataQueueHandler.ProcessHistoryRequests(): Submitting request: {0}-{1}: {2} {3}->{4}", request.Symbol.SecurityType, ticker, request.Resolution, start, end)); var span = end.Date - start.Date; var suffixes = new List(); if (span.Days < 30 && request.Resolution == Resolution.Minute) { var begin = start; while (begin < end) { suffixes.Add("date/" + begin.ToString("yyyyMMdd")); begin = begin.AddDays(1); } } else if (span.Days < 30) { suffixes.Add("1m"); } else if (span.Days < 3*30) { suffixes.Add("3m"); } else if (span.Days < 6 * 30) { suffixes.Add("6m"); } else if (span.Days < 12 * 30) { suffixes.Add("1y"); } else if (span.Days < 24 * 30) { suffixes.Add("2y"); } else { suffixes.Add("5y"); } // Download and parse data var client = new System.Net.WebClient(); foreach (var suffix in suffixes) { var response = client.DownloadString("https://cloud.iexapis.com/v1/stock/" + ticker + "/chart/" + suffix + "?token=" + _apiKey); var parsedResponse = JArray.Parse(response); foreach (var item in parsedResponse.Children()) { DateTime date; if (item["minute"] != null) { date = DateTime.ParseExact(item["date"].Value(), "yyyy-MM-dd", CultureInfo.InvariantCulture); var mins = TimeSpan.ParseExact(item["minute"].Value(), "hh\\:mm", CultureInfo.InvariantCulture); date += mins; } else { date = DateTime.Parse(item["date"].Value()); } if (date.Date < start.Date || date.Date > end.Date) { continue; } Interlocked.Increment(ref _dataPointCount); if (item["open"].Type == JTokenType.Null) { continue; } var open = item["open"].Value(); var high = item["high"].Value(); var low = item["low"].Value(); var close = item["close"].Value(); var volume = item["volume"].Value(); TradeBar tradeBar = new TradeBar(date, request.Symbol, open, high, low, close, volume); yield return new Slice(tradeBar.EndTime, new[] { tradeBar }); } } } #endregion } }