/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2026 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.Linq; using System.Threading; using QuantConnect.Util; using QuantConnect.Data; using QuantConnect.Logging; using System.Threading.Tasks; using QuantConnect.Interfaces; using QuantConnect.Configuration; using System.Collections.Generic; using System.Collections.Concurrent; using QuantConnect.Lean.Engine.HistoricalData; namespace QuantConnect.Lean.Engine.DataFeeds.DataDownloader { /// /// Decorates an to support canonical symbols by automatically /// resolving their option or future contract chains and downloading data for each constituent contract. /// public class CanonicalDataDownloaderDecorator : IDataDownloader { /// /// Prevents multiple warnings being fired when the underlying data downloader doesn't support canonical symbols. /// private bool _firedCanonicalNotSupportedWarning; /// /// Lazily initialized option chain provider for resolving option contract lists. /// private readonly IOptionChainProvider _optionChainProvider; /// /// Lazily initialized future chain provider for resolving future contract lists. /// private readonly IFutureChainProvider _futureChainProvider; /// /// The underlying data downloader that performs the actual data retrieval. /// private readonly IDataDownloader _dataDownloader; /// /// Controls parallelism for concurrent operations, /// limiting execution to a configurable number of threads (default: 4) on the default task scheduler. /// private readonly ParallelOptions _parallelOptions = new() { MaxDegreeOfParallelism = Config.GetInt("downloader-thread-count", 4), TaskScheduler = TaskScheduler.Default }; /// /// Configurable look-back period for canonical option symbols, used to limit the date range of underlying contract data downloads. /// private static readonly int _optionLookbackYeard = Config.GetInt("options-lookback-years", 1); /// /// Configurable look-back period for canonical future symbols, used to limit the date range of underlying contract data downloads. /// private static readonly int _futureLookbackYeard = Config.GetInt("futures-lookback-years", 2); /// /// Initializes a new instance of the class. /// /// The underlying data downloader to decorate with canonical symbol support. /// The data provider used for initializing chain providers. /// The map file provider used for initializing chain providers. /// The factor file provider used for initializing chain providers. public CanonicalDataDownloaderDecorator(IDataDownloader dataDownloader, IDataProvider dataProvider, IMapFileProvider mapFileProvider, IFactorFileProvider factorFileProvider) { _dataDownloader = dataDownloader; var historyManager = new HistoryProviderManager(); historyManager.Initialize( new HistoryProviderInitializeParameters( job: null, api: null, dataProvider, new SingleEntryDataCacheProvider(dataProvider, isDataEphemeral: true), mapFileProvider, factorFileProvider: factorFileProvider, // Probably not needed since canonical data doesn't require factor files statusUpdateAction: null, parallelHistoryRequestsEnabled: false, new DataPermissionManager(), objectStore: null, new AlgorithmSettings())); _optionChainProvider = Composer.Instance.GetPart(); if (_optionChainProvider == null) { var baseOptionChainProvider = new LiveOptionChainProvider(); baseOptionChainProvider.Initialize(new(mapFileProvider, historyManager)); _optionChainProvider = new CachingOptionChainProvider(baseOptionChainProvider); Composer.Instance.AddPart(_optionChainProvider); } _futureChainProvider = Composer.Instance.GetPart(); if (_futureChainProvider == null) { var baseFutureChainProvider = new BacktestingFutureChainProvider(); baseFutureChainProvider.Initialize(new(mapFileProvider, historyManager)); _futureChainProvider = new CachingFutureChainProvider(baseFutureChainProvider); Composer.Instance.AddPart(_futureChainProvider); } } /// /// Get historical data enumerable for a single symbol, type and resolution given this start and end time (in UTC). /// For canonical symbols, automatically resolves and downloads data for all underlying contracts. /// /// model class for passing in parameters for historical data /// Enumerable of base data for this symbol public IEnumerable? Get(DataDownloaderGetParameters dataDownloaderGetParameters) { var downloadedData = default(IEnumerable?); try { downloadedData = _dataDownloader.Get(dataDownloaderGetParameters); } catch (Exception ex) { Log.Error($"{nameof(CanonicalDataDownloaderDecorator)}.{nameof(Get)}.Exceptoin: {ex.Message}"); } if (downloadedData == null && dataDownloaderGetParameters.Symbol.IsCanonical()) { if (!_firedCanonicalNotSupportedWarning) { _firedCanonicalNotSupportedWarning = true; Log.Trace($"{nameof(CanonicalDataDownloaderDecorator)}.{nameof(Get)}: {_dataDownloader.GetType().Name} does not support canonical symbols. Falling back to chain provider."); } downloadedData = GetContractsData(dataDownloaderGetParameters); } return downloadedData; } /// /// Tries to adjust the date range for a given contract based on its security type and expiry date. /// The start date is clamped to a minimum look-back period and the end date is clamped to the contract expiry date. /// Returns false if the minimum look-back period exceeds the requested end date, meaning no valid range exists. /// /// The contract symbol containing the security type and expiry date. /// The requested start date in UTC. /// The requested end date in UTC. /// The adjusted start date in UTC, or default if no valid range exists. /// The adjusted end date in UTC, or default if no valid range exists. /// True if a valid adjusted date range was found, false otherwise. public static bool TryAdjustDateRangeForContract(Symbol contract, DateTime originalStartDateUtc, DateTime originalEndDateUtc, out DateTime adjustedStartDateUtc, out DateTime adjustedEndDateUtc) { var expiryDate = contract.ID.Date; var minLookBack = expiryDate; if (contract.ID.SecurityType.IsOption()) { minLookBack = expiryDate.AddYears(-_optionLookbackYeard); } else if (contract.ID.SecurityType == SecurityType.Future) { minLookBack = expiryDate.AddYears(-_futureLookbackYeard); } if (minLookBack > originalEndDateUtc || expiryDate < originalStartDateUtc) { adjustedStartDateUtc = default; adjustedEndDateUtc = default; return false; } adjustedStartDateUtc = minLookBack >= originalStartDateUtc ? minLookBack : originalStartDateUtc; adjustedEndDateUtc = expiryDate <= originalEndDateUtc ? expiryDate.AddDays(1) : originalEndDateUtc; return true; } /// /// Downloads data for all contracts of a canonical symbol in parallel, streaming results as they arrive. /// private IEnumerable? GetContractsData(DataDownloaderGetParameters parameters) { var contracts = GetContracts(parameters.Symbol, parameters.StartUtc, parameters.EndUtc); var blockingCollection = new BlockingCollection(); var processedContracts = 0L; var producerTask = Task.Run(() => { try { Parallel.ForEach( contracts, _parallelOptions, contract => { Interlocked.Increment(ref processedContracts); if (!TryAdjustDateRangeForContract(contract, parameters.StartUtc, parameters.EndUtc, out var adjustedStartDateUtc, out var adjustedEndDateUtc)) { return; } var contractParameters = new DataDownloaderGetParameters( contract, parameters.Resolution, adjustedStartDateUtc, adjustedEndDateUtc, parameters.TickType); try { var contractData = _dataDownloader.Get(contractParameters); foreach (var data in contractData) { blockingCollection.Add(data); } } catch (Exception ex) { Log.Debug($"{nameof(CanonicalDataDownloaderDecorator)}.{nameof(GetContractsData)}: " + $"Error downloading data for {contractParameters}. Exception: {ex.Message}. Continuing..."); return; } }); } finally { Log.Debug($"{nameof(CanonicalDataDownloaderDecorator)}.{nameof(GetContractsData)}: Finished downloading {processedContracts} for canonical symbol."); blockingCollection.CompleteAdding(); } }); var consumingEnumerable = blockingCollection.GetConsumingEnumerable(); if (!consumingEnumerable.Any()) { if (Interlocked.Read(ref processedContracts) == 0) { Log.Error($"{nameof(CanonicalDataDownloaderDecorator)}.{nameof(GetContractsData)}: No contracts were found. Do you have universe data?"); } return null; } return consumingEnumerable; } /// /// Retrieves unique contracts for the given canonical symbol across the specified date range. /// private IEnumerable GetContracts(Symbol symbol, DateTime startUtc, DateTime endUtc) { var chainProvider = default(Func>); if (symbol.SecurityType == SecurityType.Future) { chainProvider = _futureChainProvider.GetFutureContractList; } else if (symbol.SecurityType.IsOption()) { chainProvider = _optionChainProvider.GetOptionContractList; } else { throw new ArgumentException($"Unsupported security type {symbol.SecurityType} for canonical data downloader", nameof(symbol)); } var contractsCache = new HashSet(); foreach (var date in Time.EachDay(startUtc.Date, endUtc.Date)) { foreach (var contract in chainProvider(symbol, date)) { if (contractsCache.Add(contract)) { yield return contract; } } } } } }