69d2f5ae82
Report Generator Tests / build (push) Has been cancelled
Research Regression Tests / build (push) Has been cancelled
Python Virtual Environments / build (push) Has been cancelled
API Tests / build (push) Has been cancelled
Benchmarks / build (push) Has been cancelled
Build & Test Lean / build (push) Has been cancelled
Regression Tests / build (push) Has been cancelled
* Make FOPs selection universe file-based for backtesting * Make FOPs option chains universe file based * Make Future universe selection file-based like option universe * Make Future universe selection file-based like option universe * Abstraction cleanup * Add FuturesChains API to QC algorithm Also refactor future chain provider to use the new FutureUniverse instead of zip file names * Update regression algorithms stats * Refactor QuantBook option and future history to use new universes * Fix failing tests * Fix failing tests * Fix failing tests * Minor future chains unit test improvement * Add futures chains DataFrame property Also, remove IDerivativeSecurity interface from Future * Add DataFrame property to FuturesChains class * Add regression algorithms * Add regression algorithms * Replace QCAlgorithm.FutureChainProvider usages with new FuturesChain api * Minor fixes * Reduce number of universe files in repo * Minor data fixes * Regression algorithms updates * Add implicit conversion from FuturesContract to Symbol Modified algorithms to use futures contract objects directly instead of accessing their Symbol property. Removed unnecessary import statements and redundant lines in various files. * Improve resolution handling for history requests * Changed _auxiliaryData field to lazily-initialized AuxiliaryData property * Refactor data handling in BaseChain and TimeSliceFactory - Added `AddData` method to `BaseChain` for adding market data - Refactored `TimeSliceFactory` to use `BaseChain.AddData` method * Remove specific constructors and indexers from Chain classes Removed public indexers in `BaseChains` for getting or setting `BaseChain` instances by `ticker` or `Symbol`, which were used for Pythonnet compatibility. * Remove chain cache logic from FuturesChainUniverse * Refactor class and interface names for clarity Renamed `FileBasedUniverse` to `BaseChainUniverseData` and `IFileBasedUniverse` to `IChainUniverseData`. * Add base class for options and futures contracts - Introduced `BaseContract` as an abstract base class for contracts, consolidating common properties and methods. - Removed ISymbolInterface * Add minor fix for future options tickers parsing Added tests * Trigger Build * Trigger Build * Trigger Build * Trigger Build * Clean chain provider classes up * Remove ZipEntryName other classes and unused code Removed ZipEntryName class and references across various files. Removed DataQueueFuturesChainUniverseDataCollectionEnumerator and DataQueueOptionChainUniverseDataCollectionEnumerator classes. Removed OptionChainUniverseSubscriptionEnumeratorFactory class. Removed unused code for handling OptionChainUniverse and FuturesChainUniverse in FileSystemDataFeed.cs and LiveTradingDataFeed.cs. Removed several test files related to enumerator factories and universe data collection. * Minor changes and cleanup * Trigger Build * Trigger Build * Refactor FuturesContract data handling Forward price data from bars and ticks stored in private fields for improved memory usage * Fix: use universe data for market data in FuturesContract * Update regression algorithms stats after rebase Added HSI futures universe files * Sort configs by internal flag Internals go first * Throw from option universe data filters for future options Future options IV, Open interest and greeks are not supported for future options * Minor changes * Improve some regression algorithms * Minor fix for failing unit tests * Update FOPs universe file header Removed greeks and IV columns. Updated FOPs universe files: removed outdated columns. * Minor unit test fix * Trigger Build * Trigger Build * Trigger Build * Trigger Build * Trigger Build * Trigger Build * Trigger Build * Trigger Build * Trigger Build * Trigger Build * Trigger Build * Trigger Build * Trigger Build * Minor fix * Add history provider as constructor argument for chain providers * Update new regression algorithms data points count * Minor fix for FakeDataQueue * Add initialize method to chain providers classes * Minor changes * Trigger Build * Trigger Build * Trigger Build * Minor fix * Minor fix * Trigger Build * Trigger Build * Trigger Build * Trigger Build * Add logs to ProcessedDataProvider * Removed test logs * Minor fix * Support downloading options and futures universe files from api data provider
388 lines
18 KiB
C#
388 lines
18 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.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
|
|
{
|
|
/// <summary>
|
|
/// An implementation of <see cref="IOptionChainProvider"/> that fetches the list of contracts
|
|
/// from the Options Clearing Corporation (OCC) website
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Static constructor for the <see cref="LiveOptionChainProvider"/> class
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the option chain associated with the underlying Symbol
|
|
/// </summary>
|
|
/// <param name="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</param>
|
|
/// <param name="date">The date to ask for the option contract list for</param>
|
|
/// <returns>Option chain</returns>
|
|
/// <exception cref="ArgumentException">Option underlying Symbol is not Future or Equity</exception>
|
|
public override IEnumerable<Symbol> GetOptionContractList(Symbol symbol, DateTime date)
|
|
{
|
|
HashSet<Symbol> 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<Symbol> GetFutureOptionContractList(Symbol futureContractSymbol, DateTime date)
|
|
{
|
|
var symbols = new List<Symbol>();
|
|
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<CMEProductSlateV2ListResponse>(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<List<CMEOptionsTradeDatesAndExpiration>>(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 `<MONTH_CODE><YEAR_LAST_NUMBER>`
|
|
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<CMEOptionsExpiration, DateTime>(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<CMEOptionChainQuotes>(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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the list of option contracts for a given underlying equity symbol
|
|
/// </summary>
|
|
/// <param name="symbol">The underlying symbol</param>
|
|
/// <param name="expectedOptionTicker">The expected option ticker</param>
|
|
/// <returns>The list of option contracts</returns>
|
|
private static IEnumerable<Symbol> GetEquityIndexOptionContractList(Symbol symbol, string expectedOptionTicker)
|
|
{
|
|
var attempt = 1;
|
|
IEnumerable<Symbol> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieve the list of option contracts for an underlying symbol from the OCC website
|
|
/// </summary>
|
|
private static IEnumerable<Symbol> FindOptionContracts(Symbol underlyingSymbol, string expectedOptionTicker)
|
|
{
|
|
var symbols = new List<Symbol>();
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|