/*
* 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.Net;
using System.Linq;
using System.Net.Http;
using Newtonsoft.Json;
using System.Threading;
using QuantConnect.Util;
using QuantConnect.Logging;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
using System.Collections.Generic;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.FutureOption;
using QuantConnect.Securities.FutureOption.Api;
using System.Net.Http.Headers;
namespace QuantConnect.Lean.Engine.DataFeeds
{
///
/// An implementation of that fetches the list of contracts
/// from the Options Clearing Corporation (OCC) website
///
public class LiveOptionChainProvider : BacktestingOptionChainProvider
{
private static readonly HttpClient _client;
private static readonly DateTime _epoch = new DateTime(1970, 1, 1);
private static RateGate _cmeRateGate;
private const string CMESymbolReplace = "{{SYMBOL}}";
private const string CMEProductCodeReplace = "{{PRODUCT_CODE}}";
private const string CMEProductExpirationReplace = "{{PRODUCT_EXPIRATION}}";
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;
_client = new HttpClient(new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate });
_client.DefaultRequestHeaders.Connection.Add("keep-alive");
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*", 0.8));
_client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0");
_client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("en-US", 0.5));
}
///
/// Gets the option chain associated with the underlying Symbol
///
/// The option or the underlying symbol to get the option chain for.
/// Providing the option allows targetting an option ticker different than the default e.g. SPXW
/// The date to ask for the option contract list for
/// Option chain
/// Option underlying Symbol is not Future or Equity
public override IEnumerable GetOptionContractList(Symbol symbol, DateTime date)
{
HashSet result = null;
try
{
result = base.GetOptionContractList(symbol, date).ToHashSet();
}
catch (Exception ex)
{
result = new();
// this shouldn't happen but just in case let's log it
Log.Error(ex);
}
// during warmup we rely on the backtesting provider, but as we get closer to current time let's join the data with our live chain sources
if (date.Date >= DateTime.UtcNow.Date.AddDays(-5) || result.Count == 0)
{
var underlyingSymbol = symbol;
if (symbol.SecurityType.IsOption())
{
// we were given the option
underlyingSymbol = symbol.Underlying;
}
if (underlyingSymbol.SecurityType == SecurityType.Equity || underlyingSymbol.SecurityType == SecurityType.Index)
{
var expectedOptionTicker = underlyingSymbol.Value;
if (underlyingSymbol.SecurityType == SecurityType.Index)
{
expectedOptionTicker = symbol.ID.Symbol;
}
// Source data from TheOCC if we're trading equity or index options
foreach (var optionSymbol in GetEquityIndexOptionContractList(underlyingSymbol, expectedOptionTicker).Where(symbol => !IsContractExpired(symbol, date)))
{
result.Add(optionSymbol);
}
}
else if (underlyingSymbol.SecurityType == SecurityType.Future)
{
// We get our data from CME if we're trading future options
foreach (var optionSymbol in GetFutureOptionContractList(underlyingSymbol, date).Where(symbol => !IsContractExpired(symbol, date)))
{
result.Add(optionSymbol);
}
}
else
{
throw new ArgumentException("Option Underlying SecurityType is not supported. Supported types are: Equity, Index, Future");
}
}
foreach (var optionSymbol in result)
{
yield return optionSymbol;
}
}
private IEnumerable GetFutureOptionContractList(Symbol futureContractSymbol, DateTime date)
{
var symbols = new List();
var retries = 0;
var maxRetries = 5;
// rate gate will start a timer in the background, so let's avoid it we if don't need it
_cmeRateGate ??= new RateGate(1, TimeSpan.FromSeconds(0.5));
while (++retries <= maxRetries)
{
try
{
_cmeRateGate.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());
_cmeRateGate.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;
_cmeRateGate.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,
futureContractSymbol.SecurityType.DefaultOptionStyle(),
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 expected option ticker
/// The list of option contracts
private static IEnumerable GetEquityIndexOptionContractList(Symbol symbol, string expectedOptionTicker)
{
var attempt = 1;
IEnumerable contracts;
while (true)
{
try
{
Log.Trace($"LiveOptionChainProvider.GetOptionContractList(): Fetching option chain for option {expectedOptionTicker} underlying {symbol.Value} [Attempt {attempt}]");
contracts = FindOptionContracts(symbol, expectedOptionTicker);
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 FindOptionContracts(Symbol underlyingSymbol, string expectedOptionTicker)
{
var symbols = new List();
// 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.Value;
// download the text file
var fileContent = _client.DownloadData(url);
// read the lines, skipping the headers
var lines = fileContent.Split(new[] { "\r\n" }, StringSplitOptions.None).Skip(7);
// Example of a line:
// SPY 2021 03 26 190 000 C P 0 612 360000000
// avoid being sensitive to case
expectedOptionTicker = expectedOptionTicker.LazyToUpper();
var optionStyle = underlyingSymbol.SecurityType.DefaultOptionStyle();
// 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 != expectedOptionTicker)
{
// skip undesired options. For example SPX underlying has SPX & SPXW option tickers
continue;
}
var expiryDate = new DateTime(fields[2].ToInt32(), fields[3].ToInt32(), fields[4].ToInt32());
var strike = (fields[5] + "." + fields[6]).ToDecimal();
foreach (var right in fields[7].Trim().Split(' '))
{
OptionRight? targetRight = null;
if (right.Equals("C", StringComparison.OrdinalIgnoreCase))
{
targetRight = OptionRight.Call;
}
else if (right.Equals("P", StringComparison.OrdinalIgnoreCase))
{
targetRight = OptionRight.Put;
}
if (targetRight.HasValue)
{
symbols.Add(Symbol.CreateOption(
underlyingSymbol,
expectedOptionTicker,
underlyingSymbol.ID.Market,
optionStyle,
targetRight.Value,
strike,
expiryDate));
}
}
}
return symbols;
}
}
}