Files
quantconnect--lean/ToolBox/Polygon/PolygonDataQueueHandler.cs
T
Martin-Molinero b7df632294
Regression Tests / build (push) Has been cancelled
Build & Test Lean / build (push) Has been cancelled
Research Regression Tests / build (push) Has been cancelled
Correctly handle Polygon authentication sequence (#6485)
- Correctly handler Polygon data queue handler authentication sequence
2022-07-18 13:40:21 -03:00

1102 lines
43 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.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
{
/// <summary>
/// An implementation of <see cref="IDataQueueHandler"/> and <see cref="IHistoryProvider"/> for Polygon.io
/// </summary>
public class PolygonDataQueueHandler : SynchronizingHistoryProvider, IDataQueueHandler
{
private const string HistoryBaseUrl = "https://api.polygon.io";
private const int ResponseSizeLimitAggregateData = 50000;
private const int ResponseSizeLimitEquities = 50000;
private const int ResponseSizeLimitCurrencies = 10000;
private readonly string _apiKey = Config.Get("polygon-api-key");
private readonly IDataAggregator _dataAggregator = Composer.Instance.GetExportedValueByTypeName<IDataAggregator>(
Config.Get("data-aggregator", "QuantConnect.Lean.Engine.DataFeeds.AggregationManager"));
private readonly DataQueueHandlerSubscriptionManager _subscriptionManager;
private readonly ManualResetEvent _successfulAuthentication = new(false);
private readonly ManualResetEvent _failedAuthentication = new(false);
private readonly Dictionary<SecurityType, PolygonWebSocketClientWrapper> _webSocketClientWrappers = new();
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<Symbol, DateTimeZone> _symbolExchangeTimeZones = new Dictionary<Symbol, DateTimeZone>();
// map Polygon exchange -> Lean market
// Crypto exchanges from: https://api.polygon.io/v1/meta/crypto-exchanges?apiKey=xxx
private readonly Dictionary<int, string> _cryptoExchangeMap = new Dictionary<int, string>
{
{ 1, Market.GDAX },
{ 2, Market.Bitfinex }
};
private int _dataPointCount;
/// <summary>
/// Static constructor for the <see cref="PolygonDataQueueHandler"/> class
/// </summary>
static PolygonDataQueueHandler()
{
// Polygon.io requires TLS 1.2
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
}
/// <summary>
/// Initializes a new instance of the <see cref="PolygonDataQueueHandler"/> class
/// </summary>
public PolygonDataQueueHandler() : this(true)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PolygonDataQueueHandler"/> class
/// </summary>
public PolygonDataQueueHandler(bool streamingEnabled)
{
if (streamingEnabled)
{
foreach (var securityType in new[] { SecurityType.Equity, SecurityType.Forex, SecurityType.Crypto })
{
_failedAuthentication.Reset();
_successfulAuthentication.Reset();
_webSocketClientWrappers[securityType] = new PolygonWebSocketClientWrapper(_apiKey, _symbolMapper, securityType, OnMessage);
var timedout = WaitHandle.WaitAny(new WaitHandle[] { _failedAuthentication, _successfulAuthentication }, TimeSpan.FromMinutes(2));
if (timedout == WaitHandle.WaitTimeout)
{
ShutdownWebSockets();
throw new TimeoutException($"Timeout waiting for websocket to connect for {securityType}");
}
else if (_failedAuthentication.WaitOne(0))
{
ShutdownWebSockets();
throw new InvalidOperationException($"Websocket authentication failed for {securityType}");
}
}
}
var subscriber = new EventBasedDataQueueHandlerSubscriptionManager(t => t.ToString());
subscriber.SubscribeImpl += Subscribe;
subscriber.UnsubscribeImpl += Unsubscribe;
_subscriptionManager = subscriber;
}
#region IDataQueueHandler implementation
/// <summary>
/// Sets the job we're subscribing for
/// </summary>
/// <param name="job">Job we're subscribing for</param>
public void SetJob(LiveNodePacket job)
{
}
/// <summary>
/// Indicates the connection is live.
/// </summary>
public bool IsConnected => _webSocketClientWrappers.Count > 0 && _webSocketClientWrappers.Values.All(client => client.IsOpen);
/// <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 null;
}
var enumerator = _dataAggregator.Add(dataConfig, newDataAvailableHandler);
_subscriptionManager.Subscribe(dataConfig);
return enumerator;
}
/// <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>
/// Adds the specified symbols to the subscription
/// </summary>
/// <param name="symbols">The symbols to be added</param>
/// <param name="tickType">Type of tick data</param>
private bool Subscribe(IEnumerable<Symbol> symbols, TickType tickType)
{
foreach (var symbol in symbols)
{
var webSocket = GetWebSocket(symbol.SecurityType);
webSocket.Subscribe(symbol, tickType);
}
return true;
}
/// <summary>
/// Removes the specified symbols from the subscription
/// </summary>
/// <param name="symbols">The symbols to be removed</param>
/// <param name="tickType">Type of tick data</param>
private bool Unsubscribe(IEnumerable<Symbol> symbols, TickType tickType)
{
foreach (var symbol in symbols)
{
var webSocket = GetWebSocket(symbol.SecurityType);
webSocket.Unsubscribe(symbol, tickType);
}
return true;
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
ShutdownWebSockets();
_dataAggregator.DisposeSafely();
}
#endregion
#region IHistoryProvider implementation
/// <summary>
/// Gets the total number of data points emitted by this history provider
/// </summary>
public override int DataPointCount => _dataPointCount;
/// <summary>
/// Initializes this history provider to work for the specified job
/// </summary>
/// <param name="parameters">The initialization parameters</param>
public override void Initialize(HistoryProviderInitializeParameters parameters)
{
}
/// <summary>
/// Gets the history for the requested securities
/// </summary>
/// <param name="requests">The historical data requests</param>
/// <param name="sliceTimeZone">The time zone used when time stamping the slice instances</param>
/// <returns>An enumerable of the slices of data covering the span specified in each request</returns>
public override IEnumerable<Slice> GetHistory(IEnumerable<HistoryRequest> requests, DateTimeZone sliceTimeZone)
{
var subscriptions = new List<Subscription>();
foreach (var request in requests)
{
var history = GetHistory(request);
var subscription = CreateSubscription(request, history);
subscriptions.Add(subscription);
}
return CreateSliceEnumerableFromSubscriptions(subscriptions, sliceTimeZone);
}
/// <summary>
/// Gets the history for the requested security
/// </summary>
/// <param name="request">The historical data request</param>
/// <returns>An enumerable of BaseData points</returns>
public IEnumerable<BaseData> GetHistory(HistoryRequest request)
{
return ProcessHistoryRequest(request);
}
#endregion
private IEnumerable<BaseData> 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<Tick> GetQuoteTicks(HistoryRequest request)
{
switch (request.Symbol.SecurityType)
{
case SecurityType.Equity:
return GetEquityQuoteTicks(request);
case SecurityType.Forex:
return GetForexQuoteTicks(request);
default:
return Enumerable.Empty<Tick>();
}
}
private IEnumerable<Tick> GetTradeTicks(HistoryRequest request)
{
switch (request.Symbol.SecurityType)
{
case SecurityType.Equity:
return GetEquityTradeTicks(request);
case SecurityType.Crypto:
return GetCryptoTradeTicks(request);
default:
return Enumerable.Empty<Tick>();
}
}
private IEnumerable<Tick> GetForexQuoteTicks(HistoryRequest request)
{
// https://api.polygon.io/v1/historic/forex/EUR/USD/2020-08-24?apiKey=
var start = request.StartTimeUtc;
var end = request.EndTimeUtc;
var currentDate = start.Date;
while (currentDate <= end.Date)
{
Log.Debug($"GetForexQuoteTicks(): Downloading ticks for the date {currentDate:yyyy-MM-dd}; symbol: {request.Symbol.ID.Symbol}");
// If this is a very first iteration set offset exactly as request's start time.
// Otherwise use date start as an offset. (!) Make sure to cast to Int64.
var offset = currentDate == start.Date ? (long)Time.DateTimeToUnixTimeStampMilliseconds(start)
: (long)Time.DateTimeToUnixTimeStampMilliseconds(currentDate);
var counter = 0;
long lastTickTimestamp = 0;
while (true)
{
counter++;
string baseCurrency;
string quoteCurrency;
Forex.DecomposeCurrencyPair(request.Symbol.Value, out baseCurrency, out quoteCurrency);
var url = $"{HistoryBaseUrl}/v1/historic/forex/{baseCurrency}/{quoteCurrency}/{currentDate:yyyy-MM-dd}?" +
$"limit={ResponseSizeLimitCurrencies}&apiKey={_apiKey}&offset={offset}";
var response = DownloadAndParseData(typeof(ForexQuoteTickResponse[]), url, "ticks") as ForexQuoteTickResponse[];
// The first results of the next page will coincide with last of the previous page, lets clear from repeating values
var quoteTicksList = response?.Where(x => x.Timestamp != lastTickTimestamp).ToList();
if (quoteTicksList.IsNullOrEmpty())
{
break;
}
Log.Debug($"GetForexQuoteTicks(): Page # {counter}; " +
$"first: {Time.UnixMillisecondTimeStampToDateTime(quoteTicksList.First().Timestamp)}; " +
$"last: {Time.UnixMillisecondTimeStampToDateTime(quoteTicksList.Last().Timestamp)}");
foreach (var row in quoteTicksList)
{
var utcTime = Time.UnixMillisecondTimeStampToDateTime(row.Timestamp);
if (utcTime < start)
{
continue;
}
if (utcTime > end)
{
yield break;
}
var time = GetTickTime(request.Symbol, utcTime);
yield return new Tick(time, request.Symbol, row.Bid, row.Ask);
lastTickTimestamp = row.Timestamp;
}
offset = lastTickTimestamp;
_dataPointCount += quoteTicksList.Count;
}
// Jump to the next iteration
currentDate = currentDate.AddDays(1);
}
}
private IEnumerable<Tick> GetCryptoTradeTicks(HistoryRequest request)
{
// https://api.polygon.io/v1/historic/crypto/BTC/USD/2020-08-24?apiKey=
var start = request.StartTimeUtc;
var end = request.EndTimeUtc;
var currentDate = start.Date;
while (currentDate <= end.Date)
{
Log.Debug(
$"GetCryptoTradeTicks(): Downloading ticks for the date {currentDate:yyyy-MM-dd}; symbol: {request.Symbol.ID.Symbol}");
var offset = currentDate == start.Date ? (long)Time.DateTimeToUnixTimeStampMilliseconds(start)
: (long)Time.DateTimeToUnixTimeStampMilliseconds(currentDate);
var counter = 0;
long lastTickTimestamp = 0;
while (true)
{
counter++;
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 url = $"{HistoryBaseUrl}/v1/historic/crypto/{baseCurrency}/{quoteCurrency}/{currentDate:yyyy-MM-dd}?" +
$"limit={ResponseSizeLimitCurrencies}&apiKey={_apiKey}&offset={offset}";
var response = DownloadAndParseData(typeof(CryptoTradeTickResponse[]), url, "ticks") as CryptoTradeTickResponse[];
// The first results of the next page will coincide with last of the previous page, lets clear from repeating values
var tradeTicksList = response?.Where(x => x.Timestamp != lastTickTimestamp).ToList();
if (tradeTicksList.IsNullOrEmpty())
{
break;
}
Log.Debug($"GetCryptoTradeTicks(): Page # {counter}; " +
$"first: {Time.UnixMillisecondTimeStampToDateTime(tradeTicksList.First().Timestamp)}; " +
$"last: {Time.UnixMillisecondTimeStampToDateTime(tradeTicksList.Last().Timestamp)}");
foreach (var row in tradeTicksList)
{
var utcTime = Time.UnixMillisecondTimeStampToDateTime(row.Timestamp);
if (utcTime < start)
{
continue;
}
if (utcTime > end)
{
yield break;
}
// Another oddity of coin api. The final tick of the dat may be a tick from another exchange
// If we are geting such a tick, we need to store the last tick value in due course.
var market = GetMarketFromCryptoExchangeId(row.Exchange);
if (market != request.Symbol.ID.Market)
{
lastTickTimestamp = row.Timestamp;
continue;
}
var time = GetTickTime(request.Symbol, utcTime);
yield return new Tick(time, request.Symbol, string.Empty, string.Empty, row.Size, row.Price);
lastTickTimestamp = row.Timestamp;
}
offset = lastTickTimestamp;
_dataPointCount += tradeTicksList.Count;
}
// Jump to the next iteration
currentDate = currentDate.AddDays(1);
}
}
private IEnumerable<Tick> GetEquityQuoteTicks(HistoryRequest request)
{
// https://api.polygon.io/v2/ticks/stocks/nbbo/SPY/2020-08-24?apiKey=
var start = request.StartTimeUtc;
var end = request.EndTimeUtc;
var currentDate = start.Date;
while (currentDate <= end.Date)
{
Log.Debug($"GetEquityQuoteTicks(): Downloading ticks for the date {currentDate:yyyy-MM-dd}; symbol: {request.Symbol.ID.Symbol}");
// If this is a very first iteration set offset exactly as request's start time. Otherwise use date start as an offset.
var offset = currentDate == start.Date
? Time.DateTimeToUnixTimeStampNanoseconds(start)
: Time.DateTimeToUnixTimeStampNanoseconds(currentDate);
var counter = 0;
long lastTickSipTimeStamp = 0;
while (true)
{
counter++;
var url = $"{HistoryBaseUrl}/v2/ticks/stocks/nbbo/{request.Symbol.Value}/{currentDate.Date:yyyy-MM-dd}?" +
$"apiKey={_apiKey}&timestamp={offset}&limit={ResponseSizeLimitEquities}";
var response = DownloadAndParseData(typeof(EquityQuoteTickResponse[]), url, "results") as EquityQuoteTickResponse[];
// The first results of the next page will coincide with last of the previous page
// We distinguish the results by the timestamp, lets clear from repeating values
var quoteTicksList = response?.Where(x => x.SipTimestamp != lastTickSipTimeStamp).ToList();
// API will send at the end only such repeating ticks that coincide with last results of previous page
// If there are no other ticks other than these then we break
if (quoteTicksList.IsNullOrEmpty())
{
break;
}
Log.Debug($"GetEquityQuoteTicks(): Page # {counter}; " +
$"first: {Time.UnixNanosecondTimeStampToDateTime(quoteTicksList.First().SipTimestamp)}; " +
$"last: {Time.UnixNanosecondTimeStampToDateTime(quoteTicksList.Last().SipTimestamp)}");
foreach (var row in quoteTicksList)
{
var utcTime = Time.UnixNanosecondTimeStampToDateTime(row.SipTimestamp);
if (utcTime < start)
{
continue;
}
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);
// Save the values before to jump to the next iteration
lastTickSipTimeStamp = row.SipTimestamp;
}
offset = lastTickSipTimeStamp;
_dataPointCount += quoteTicksList.Count;
}
// Jump to the next iteration
currentDate = currentDate.AddDays(1);
}
}
private IEnumerable<Tick> GetEquityTradeTicks(HistoryRequest request)
{
// https://api.polygon.io/v2/ticks/stocks/trades/SPY/2020-08-24?apiKey=
var start = request.StartTimeUtc;
var end = request.EndTimeUtc;
var currentDate = start.Date;
while (currentDate <= end.Date)
{
Log.Debug($"GetEquityTradeTicks(): Downloading ticks for the date {currentDate:yyyy-MM-dd}; symbol: {request.Symbol.ID.Symbol}");
// If this is a very first iteration set offset exactly as request's start time. Otherwise use date start as an offset.
var offset = currentDate == start.Date
? Time.DateTimeToUnixTimeStampNanoseconds(start)
: Time.DateTimeToUnixTimeStampNanoseconds(currentDate);
var counter = 0;
long lastTickSipTimeStamp = 0;
while (true)
{
counter++;
var url = $"{HistoryBaseUrl}/v2/ticks/stocks/trades/{request.Symbol.ID.Symbol}/{currentDate:yyyy-MM-dd}?" +
$"apiKey={_apiKey}&timestamp={offset}&limit={ResponseSizeLimitEquities}";
var response = DownloadAndParseData(typeof(EquityTradeTickResponse[]), url, "results") as EquityTradeTickResponse[];
// The first results of the next page will coincide with last of the previous page
// We distinguish the results by the timestamp, lets clear from repeating values
var tradeTicksList = response?.Where(x => x.SipTimestamp != lastTickSipTimeStamp).ToList();
// API will send at the end only such repeating ticks that coincide with last results of previous page
// If there are no other ticks other than these then we break
if (tradeTicksList.IsNullOrEmpty())
{
break;
}
Log.Debug($"GetEquityTradeTicks(): Page # {counter}; " +
$"first: {Time.UnixNanosecondTimeStampToDateTime(tradeTicksList.First().SipTimestamp)}; " +
$"last: {Time.UnixNanosecondTimeStampToDateTime(tradeTicksList.Last().SipTimestamp)}");
foreach (var row in tradeTicksList)
{
var utcTime = Time.UnixNanosecondTimeStampToDateTime(row.SipTimestamp);
if (utcTime < start)
{
continue;
}
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);
// Save the values before to jump to the next iteration
lastTickSipTimeStamp = row.SipTimestamp;
}
offset = lastTickSipTimeStamp;
_dataPointCount += tradeTicksList.Count;
}
// Jump to the next iteration
currentDate = currentDate.AddDays(1);
}
}
private IEnumerable<TradeBar> 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 resolutionTimeSpan = request.Resolution.ToTimeSpan();
var lastRequestedBarStartTime = request.EndTimeUtc.RoundDown(resolutionTimeSpan);
var start = request.StartTimeUtc.Date;
var end = lastRequestedBarStartTime;
// Perform a check of the number of bars requested, this must not exceed a static limit
var aggregatesCountPerResolution = GetAggregatesCountPerReselection(request.Resolution);
var dataRequestedCount = (end - start).Ticks
/ resolutionTimeSpan.Ticks / aggregatesCountPerResolution;
if (dataRequestedCount > ResponseSizeLimitAggregateData)
{
end = start + TimeSpan.FromTicks(resolutionTimeSpan.Ticks * ResponseSizeLimitAggregateData / aggregatesCountPerResolution);
end = end.Date;
}
while (start < lastRequestedBarStartTime.Date)
{
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}&limit={ResponseSizeLimitAggregateData}";
var aggregatesResponse = DownloadAndParseData(typeof(AggregatesResponse), url) as AggregatesResponse;
var rows = aggregatesResponse?.Results;
if (rows != null)
{
foreach (var row in rows)
{
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);
}
}
start = end.AddDays(1);
end += TimeSpan.FromTicks(resolutionTimeSpan.Ticks * ResponseSizeLimitAggregateData / aggregatesCountPerResolution);
if (end > lastRequestedBarStartTime)
{
end = lastRequestedBarStartTime;
}
end = end.Date;
}
}
private static IEnumerable<TradeBar> AggregateTradeTicks(Symbol symbol, IEnumerable<Tick> 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<QuoteBar> AggregateQuoteTicks(Symbol symbol, IEnumerable<Tick> 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 InvalidOperationException($"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<EquityTradeMessage>());
break;
case "Q":
ProcessEquityQuote(obj.ToObject<EquityQuoteMessage>());
break;
case "C":
ProcessForexQuote(obj.ToObject<ForexQuoteMessage>());
break;
case "XT":
ProcessCryptoTrade(obj.ToObject<CryptoTradeMessage>());
break;
case "XQ":
ProcessCryptoQuote(obj.ToObject<CryptoQuoteMessage>());
break;
case "status":
var jstatus = obj["status"];
if (jstatus != null && jstatus.Type == JTokenType.String)
{
var status = jstatus.ToString();
if (status.Contains("auth_failed", StringComparison.InvariantCultureIgnoreCase))
{
var errorMessage = string.Empty;
var jmessage = obj["message"];
if (jmessage != null)
{
errorMessage = jmessage.ToString();
}
Log.Error($"PolygonDataQueueHandler(): authentication failed: '{errorMessage}'.");
_failedAuthentication.Set();
}
else if (status.Contains("auth_success", StringComparison.InvariantCultureIgnoreCase))
{
Log.Trace($"PolygonDataQueueHandler(): successful authentication.");
_successfulAuthentication.Set();
}
}
break;
}
}
}
private void ShutdownWebSockets()
{
foreach (var websocket in _webSocketClientWrappers)
{
websocket.Value.Close();
}
_webSocketClientWrappers.Clear();
}
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;
}
private static int GetAggregatesCountPerReselection(Resolution resolution)
{
switch (resolution)
{
case Resolution.Minute:
return 1;
case Resolution.Hour:
return 60;
case Resolution.Daily:
return 1;
default:
throw new NotSupportedException($"No data aggregation for {resolution} resolution");
}
}
private static object DownloadAndParseData(Type type, string url, string jsonPropertyName = null)
{
var result = url.DownloadData();
if (result == null)
{
return null;
}
if (jsonPropertyName != null)
{
result = JObject.Parse(result)[jsonPropertyName]?.ToString();
}
return result == null ? null : JsonConvert.DeserializeObject(result, type);
}
}
}