/* * 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.Net.Http; using System.Threading; using Newtonsoft.Json; using QuantConnect.Interfaces; using QuantConnect.Logging; using QuantConnect.Securities; using QuantConnect.Securities.Future; using QuantConnect.Securities.FutureOption; using QuantConnect.Securities.FutureOption.Api; using QuantConnect.Util; namespace QuantConnect.Lean.Engine.DataFeeds { /// /// An implementation of that fetches the list of contracts /// from the Options Clearing Corporation (OCC) website /// public class LiveOptionChainProvider : IOptionChainProvider { private static readonly HttpClient _client = new HttpClient(); private static readonly DateTime _epoch = new DateTime(1970, 1, 1); private readonly RateGate _rateGate = new RateGate(1, TimeSpan.FromSeconds(0.5)); private const string CMESymbolReplace = "{{SYMBOL}}"; private const string CMEProductCodeReplace = "{{PRODUCT_CODE}}"; private const string CMEContractCodeReplace = "{{CONTRACT_CODE}}"; private const string CMEProductExpirationReplace = "{{PRODUCT_EXPIRATION}}"; private const string CMEDateTimeReplace = "{{DT_REPLACE}}"; private const string CMEProductSlateURL = "https://www.cmegroup.com/CmeWS/mvc/ProductSlate/V2/List?pageNumber=1&sortAsc=false&sortField=rank&searchString=" + CMESymbolReplace + "&pageSize=5"; private const string CMEOptionsTradeDateAndExpirations = "https://www.cmegroup.com/CmeWS/mvc/Settlements/Options/TradeDateAndExpirations/" + CMEProductCodeReplace; private const string CMEOptionChainQuotesURL = "https://www.cmegroup.com/CmeWS/mvc/Quotes/Option/" + CMEProductCodeReplace + "/G/" + CMEProductExpirationReplace + "/ALL?_="; private const int MaxDownloadAttempts = 5; /// /// Static constructor for the class /// static LiveOptionChainProvider() { // The OCC website now requires at least TLS 1.1 for API requests. // NET 4.5.2 and below does not enable these more secure protocols by default, so we add them in here ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; } /// /// Gets the option chain associated with the underlying Symbol /// /// Underlying symbol to get the option chain for /// Unused /// Option chain /// Option underlying Symbol is not Future or Equity public IEnumerable GetOptionContractList(Symbol underlyingSymbol, DateTime date) { if (underlyingSymbol.SecurityType == SecurityType.Equity) { // Source data from TheOCC if we're trading equity options return GetEquityOptionContractList(underlyingSymbol, date); } if (underlyingSymbol.SecurityType == SecurityType.Future) { // We get our data from CME if we're trading future options return GetFutureOptionContractList(underlyingSymbol, date); } throw new ArgumentException("Option Underlying SecurityType is not supported. Supported types are: Equity, Future"); } private IEnumerable GetFutureOptionContractList(Symbol futureContractSymbol, DateTime date) { var symbols = new List(); var retries = 0; var maxRetries = 5; while (++retries <= maxRetries) { try { _rateGate.WaitToProceed(); var productResponse = _client.GetAsync(CMEProductSlateURL.Replace(CMESymbolReplace, futureContractSymbol.ID.Symbol)) .SynchronouslyAwaitTaskResult(); productResponse.EnsureSuccessStatusCode(); var productResults = JsonConvert.DeserializeObject(productResponse.Content .ReadAsStringAsync() .SynchronouslyAwaitTaskResult()); productResponse.Dispose(); // We want to gather the future product to get the future options ID var futureProductId = productResults.Products.Where(p => p.Globex == futureContractSymbol.ID.Symbol && p.GlobexTraded && p.Cleared == "Futures") .Select(p => p.Id) .Single(); var optionsTradesAndExpiries = CMEOptionsTradeDateAndExpirations.Replace(CMEProductCodeReplace, futureProductId.ToStringInvariant()); _rateGate.WaitToProceed(); var optionsTradesAndExpiriesResponse = _client.GetAsync(optionsTradesAndExpiries).SynchronouslyAwaitTaskResult(); optionsTradesAndExpiriesResponse.EnsureSuccessStatusCode(); var tradesAndExpiriesResponse = JsonConvert.DeserializeObject>(optionsTradesAndExpiriesResponse.Content .ReadAsStringAsync() .SynchronouslyAwaitTaskResult()); optionsTradesAndExpiriesResponse.Dispose(); // For now, only support American options on CME var selectedOption = tradesAndExpiriesResponse .FirstOrDefault(x => !x.Daily && !x.Weekly && !x.Sto && x.OptionType == "AME"); if (selectedOption == null) { Log.Error($"LiveOptionChainProvider.GetFutureOptionContractList(): Found no matching future options for contract {futureContractSymbol}"); yield break; } // Gather the month code and the year's last number to query the next API, which expects an expiration as `` var canonicalFuture = Symbol.Create(futureContractSymbol.ID.Symbol, SecurityType.Future, futureContractSymbol.ID.Market); var expiryFunction = FuturesExpiryFunctions.FuturesExpiryFunction(canonicalFuture); var futureContractExpiration = selectedOption.Expirations .Select(x => new KeyValuePair(x, expiryFunction(new DateTime(x.Expiration.Year, x.Expiration.Month, 1)))) .FirstOrDefault(x => x.Value.Year == futureContractSymbol.ID.Date.Year && x.Value.Month == futureContractSymbol.ID.Date.Month) .Key; if (futureContractExpiration == null) { Log.Error($"LiveOptionChainProvider.GetFutureOptionContractList(): Found no future options with matching expiry year and month for contract {futureContractSymbol}"); yield break; } var futureContractMonthCode = futureContractExpiration.Expiration.Code; _rateGate.WaitToProceed(); // Subtract one day from now for settlement API since settlement may not be available for today yet var optionChainQuotesResponseResult = _client.GetAsync(CMEOptionChainQuotesURL .Replace(CMEProductCodeReplace, selectedOption.ProductId.ToStringInvariant()) .Replace(CMEProductExpirationReplace, futureContractMonthCode) + Math.Floor((DateTime.UtcNow - _epoch).TotalMilliseconds).ToStringInvariant()); optionChainQuotesResponseResult.Result.EnsureSuccessStatusCode(); var futureOptionChain = JsonConvert.DeserializeObject(optionChainQuotesResponseResult.Result.Content .ReadAsStringAsync() .SynchronouslyAwaitTaskResult()) .Quotes .DistinctBy(s => s.StrikePrice) .ToList(); optionChainQuotesResponseResult.Dispose(); // Each CME contract can have arbitrary scaling applied to the strike price, so we normalize it to the // underlying's price via static entries. var optionStrikePriceScaleFactor = CMEStrikePriceScalingFactors.GetScaleFactor(futureContractSymbol); var canonicalOption = Symbol.CreateOption( futureContractSymbol, futureContractSymbol.ID.Market, default(OptionStyle), default(OptionRight), default(decimal), SecurityIdentifier.DefaultDate); foreach (var optionChainEntry in futureOptionChain) { var futureOptionExpiry = FuturesOptionsExpiryFunctions.GetFutureOptionExpiryFromFutureExpiry(futureContractSymbol, canonicalOption); var scaledStrikePrice = optionChainEntry.StrikePrice / optionStrikePriceScaleFactor; // Calls and puts share the same strike, create two symbols per each to avoid iterating twice. symbols.Add(Symbol.CreateOption( futureContractSymbol, futureContractSymbol.ID.Market, OptionStyle.American, OptionRight.Call, scaledStrikePrice, futureOptionExpiry)); symbols.Add(Symbol.CreateOption( futureContractSymbol, futureContractSymbol.ID.Market, OptionStyle.American, OptionRight.Put, scaledStrikePrice, futureOptionExpiry)); } break; } catch (HttpRequestException err) { if (retries != maxRetries) { Log.Error(err, $"Failed to retrieve futures options chain from CME, retrying ({retries} / {maxRetries})"); continue; } Log.Error(err, $"Failed to retrieve futures options chain from CME, returning empty result ({retries} / {retries})"); } } foreach (var symbol in symbols) { yield return symbol; } } /// /// Gets the list of option contracts for a given underlying equity symbol /// /// The underlying symbol /// The date for which to request the option chain (only used in backtesting) /// The list of option contracts private IEnumerable GetEquityOptionContractList(Symbol symbol, DateTime date) { var attempt = 1; IEnumerable contracts; while (true) { try { Log.Trace($"LiveOptionChainProvider.GetOptionContractList(): Fetching option chain for {symbol.Value} [Attempt {attempt}]"); contracts = FindEquityOptionContracts(symbol.Value); break; } catch (WebException exception) { Log.Error(exception); if (++attempt > MaxDownloadAttempts) { throw; } Thread.Sleep(1000); } } return contracts; } /// /// Retrieve the list of option contracts for an underlying symbol from the OCC website /// private static IEnumerable FindEquityOptionContracts(string underlyingSymbol) { var symbols = new List(); using (var client = new WebClient()) { // use QC url to bypass TLS issues with Mono pre-4.8 version var url = "https://www.quantconnect.com/api/v2/theocc/series-search?symbolType=U&symbol=" + underlyingSymbol; // download the text file var fileContent = client.DownloadString(url); // read the lines, skipping the headers var lines = fileContent.Split(new[] { "\r\n" }, StringSplitOptions.None).Skip(7); // parse the lines, creating the Lean option symbols foreach (var line in lines) { var fields = line.Split('\t'); var ticker = fields[0].Trim(); if (ticker != underlyingSymbol) continue; var expiryDate = new DateTime(fields[2].ToInt32(), fields[3].ToInt32(), fields[4].ToInt32()); var strike = (fields[5] + "." + fields[6]).ToDecimal(); if (fields[7].Contains("C")) { symbols.Add(Symbol.CreateOption(underlyingSymbol, Market.USA, OptionStyle.American, OptionRight.Call, strike, expiryDate)); } if (fields[7].Contains("P")) { symbols.Add(Symbol.CreateOption(underlyingSymbol, Market.USA, OptionStyle.American, OptionRight.Put, strike, expiryDate)); } } } return symbols; } } }