From 6b4dcf078f82529423f876f0e454bbbdaa625c11 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 19 Aug 2026 10:58:41 -0400 Subject: [PATCH 1/3] Support custom option chain provider through universe-option-chain-provider config --- .../ChainSymbolProvider.cs | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/Lean.DataSource.DerivativeUniverseGenerator/ChainSymbolProvider.cs b/Lean.DataSource.DerivativeUniverseGenerator/ChainSymbolProvider.cs index 927e627..a8a191e 100644 --- a/Lean.DataSource.DerivativeUniverseGenerator/ChainSymbolProvider.cs +++ b/Lean.DataSource.DerivativeUniverseGenerator/ChainSymbolProvider.cs @@ -30,9 +30,11 @@ namespace QuantConnect.DataSource.DerivativeUniverseGenerator public class ChainSymbolProvider { private readonly IDataCacheProvider _dataCacheProvider; + private readonly IOptionChainProvider _optionChainProvider; protected readonly DateTime _processingDate; protected readonly string _dataSourceFolder; protected readonly SecurityType _securityType; + protected readonly string _market; // 99% of cases will use quote zip files to get the contracts, but in rear cases we may need to use trade zip files. e.g EUREX data protected TickType[] _symbolsDataTickTypes = { TickType.Quote, TickType.Trade }; @@ -53,14 +55,50 @@ public ChainSymbolProvider(IDataCacheProvider dataCacheProvider, DateTime proces { _processingDate = processingDate; _securityType = securityType; + _market = market; _dataSourceFolder = Path.Combine(dataFolderRoot, securityType.SecurityTypeToLower(), market); _dataCacheProvider = dataCacheProvider; + + if (securityType.IsOption() && + Config.TryGetValue("universe-option-chain-provider", out var optionChainProviderStr) && + !string.IsNullOrEmpty(optionChainProviderStr)) + { + _optionChainProvider = Composer.Instance.GetExportedValueByTypeName(optionChainProviderStr); + } } /// /// Gets all the available symbols keyed by the canonical symbol from the available price data in the data folder. /// public virtual Dictionary> GetSymbols() + { + if (_optionChainProvider == null) + { + return GetSymbolsFromDataFiles(); + } + + // A null symbol fetches the contracts of every canonical the provider finds + var contracts = _optionChainProvider.GetOptionContractList(null, _processingDate)?.ToList(); + if (contracts == null || contracts.Count == 0) + { + // The custom chain provider failed, fallback to the file-based chains + return GetSymbolsFromDataFiles(); + } + + return contracts + .Where(symbol => symbol.SecurityType == _securityType + && symbol.ID.Market == _market + // do not return expired contracts + && _processingDate.Date < symbol.ID.Date.Date) + .Distinct() + .GroupBy(symbol => symbol.Canonical) + .ToDictionary(group => group.Key, group => OrderSymbols(group, _securityType).ToList()); + } + + /// + /// Gets all the available symbols keyed by the canonical symbol from the available price data in the data folder. + /// + private Dictionary> GetSymbolsFromDataFiles() { var result = new Dictionary>(); @@ -172,19 +210,23 @@ private List GetSymbolsFromZipEntryNames(string zipFileName, Symbol cano .Where(symbol => _processingDate.Date < symbol.ID.Date.Date) .Distinct(); - if (canonicalSymbol.SecurityType.IsOption()) + return OrderSymbols(symbols, canonicalSymbol.SecurityType).ToList(); + } + + /// + /// Orders the given chain of contracts. + /// + private static IEnumerable OrderSymbols(IEnumerable symbols, SecurityType securityType) + { + if (securityType.IsOption()) { - symbols = symbols.OrderBy(symbol => symbol.ID.OptionRight) + return symbols.OrderBy(symbol => symbol.ID.OptionRight) .ThenBy(symbol => symbol.ID.Date) .ThenBy(symbol => symbol.ID.StrikePrice) .ThenBy(symbol => symbol.ID); } - else - { - symbols = symbols.OrderBy(symbol => symbol.ID.Date).ThenBy(symbol => symbol.ID); - } - return symbols.ToList(); + return symbols.OrderBy(symbol => symbol.ID.Date).ThenBy(symbol => symbol.ID); } } } From 5c59b7f4ba116ec6a98c935112da759702851e8d Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 19 Aug 2026 17:50:57 -0400 Subject: [PATCH 2/3] Request chains with a tickerless dummy symbol --- .../ChainSymbolProvider.cs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/Lean.DataSource.DerivativeUniverseGenerator/ChainSymbolProvider.cs b/Lean.DataSource.DerivativeUniverseGenerator/ChainSymbolProvider.cs index a8a191e..0ee6dc4 100644 --- a/Lean.DataSource.DerivativeUniverseGenerator/ChainSymbolProvider.cs +++ b/Lean.DataSource.DerivativeUniverseGenerator/ChainSymbolProvider.cs @@ -77,8 +77,9 @@ public virtual Dictionary> GetSymbols() return GetSymbolsFromDataFiles(); } - // A null symbol fetches the contracts of every canonical the provider finds - var contracts = _optionChainProvider.GetOptionContractList(null, _processingDate)?.ToList(); + // A tickerless dummy symbol fetches the contracts of every canonical of the + // generator's security type and market the provider finds + var contracts = _optionChainProvider.GetOptionContractList(CreateChainsRequestSymbol(), _processingDate)?.ToList(); if (contracts == null || contracts.Count == 0) { // The custom chain provider failed, fallback to the file-based chains @@ -95,6 +96,30 @@ public virtual Dictionary> GetSymbols() .ToDictionary(group => group.Key, group => OrderSymbols(group, _securityType).ToList()); } + /// + /// Creates the tickerless dummy symbol used to request the chains of every canonical of the + /// generator's security type and market from the custom chain provider + /// + private Symbol CreateChainsRequestSymbol() + { + Symbol underlying; + switch (_securityType) + { + case SecurityType.Option: + // equity SID generation must skip mapping, which rejects empty tickers + underlying = new Symbol(SecurityIdentifier.GenerateEquity(string.Empty, _market, mapSymbol: false), string.Empty); + break; + case SecurityType.IndexOption: + underlying = Symbol.Create(string.Empty, SecurityType.Index, _market); + break; + default: + throw new NotSupportedException($"ChainSymbolProvider.CreateChainsRequestSymbol(): " + + $"unsupported security type {_securityType}"); + } + + return Symbol.CreateCanonicalOption(underlying); + } + /// /// Gets all the available symbols keyed by the canonical symbol from the available price data in the data folder. /// From 8cf345e12fd17226f9ba033354d375a68ddabf01 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 19 Aug 2026 19:32:21 -0400 Subject: [PATCH 3/3] Move custom option chain provider support to new OptionChainSymbolProvider --- .../ChainSymbolProvider.cs | 67 +---------- .../DerivativeUniverseGenerator.cs | 6 +- .../OptionChainSymbolProvider.cs | 104 ++++++++++++++++++ .../OptionsUniverseGenerator.cs | 6 + .../DerivativeUniverseGeneratorTests.cs | 5 + 5 files changed, 118 insertions(+), 70 deletions(-) create mode 100644 Lean.DataSource.OptionsUniverseGenerator/OptionChainSymbolProvider.cs diff --git a/Lean.DataSource.DerivativeUniverseGenerator/ChainSymbolProvider.cs b/Lean.DataSource.DerivativeUniverseGenerator/ChainSymbolProvider.cs index 0ee6dc4..2bc3e0c 100644 --- a/Lean.DataSource.DerivativeUniverseGenerator/ChainSymbolProvider.cs +++ b/Lean.DataSource.DerivativeUniverseGenerator/ChainSymbolProvider.cs @@ -27,14 +27,12 @@ namespace QuantConnect.DataSource.DerivativeUniverseGenerator /// /// File based symbol chain provider /// - public class ChainSymbolProvider + public abstract class ChainSymbolProvider { private readonly IDataCacheProvider _dataCacheProvider; - private readonly IOptionChainProvider _optionChainProvider; protected readonly DateTime _processingDate; protected readonly string _dataSourceFolder; protected readonly SecurityType _securityType; - protected readonly string _market; // 99% of cases will use quote zip files to get the contracts, but in rear cases we may need to use trade zip files. e.g EUREX data protected TickType[] _symbolsDataTickTypes = { TickType.Quote, TickType.Trade }; @@ -55,75 +53,14 @@ public ChainSymbolProvider(IDataCacheProvider dataCacheProvider, DateTime proces { _processingDate = processingDate; _securityType = securityType; - _market = market; _dataSourceFolder = Path.Combine(dataFolderRoot, securityType.SecurityTypeToLower(), market); _dataCacheProvider = dataCacheProvider; - - if (securityType.IsOption() && - Config.TryGetValue("universe-option-chain-provider", out var optionChainProviderStr) && - !string.IsNullOrEmpty(optionChainProviderStr)) - { - _optionChainProvider = Composer.Instance.GetExportedValueByTypeName(optionChainProviderStr); - } } /// /// Gets all the available symbols keyed by the canonical symbol from the available price data in the data folder. /// public virtual Dictionary> GetSymbols() - { - if (_optionChainProvider == null) - { - return GetSymbolsFromDataFiles(); - } - - // A tickerless dummy symbol fetches the contracts of every canonical of the - // generator's security type and market the provider finds - var contracts = _optionChainProvider.GetOptionContractList(CreateChainsRequestSymbol(), _processingDate)?.ToList(); - if (contracts == null || contracts.Count == 0) - { - // The custom chain provider failed, fallback to the file-based chains - return GetSymbolsFromDataFiles(); - } - - return contracts - .Where(symbol => symbol.SecurityType == _securityType - && symbol.ID.Market == _market - // do not return expired contracts - && _processingDate.Date < symbol.ID.Date.Date) - .Distinct() - .GroupBy(symbol => symbol.Canonical) - .ToDictionary(group => group.Key, group => OrderSymbols(group, _securityType).ToList()); - } - - /// - /// Creates the tickerless dummy symbol used to request the chains of every canonical of the - /// generator's security type and market from the custom chain provider - /// - private Symbol CreateChainsRequestSymbol() - { - Symbol underlying; - switch (_securityType) - { - case SecurityType.Option: - // equity SID generation must skip mapping, which rejects empty tickers - underlying = new Symbol(SecurityIdentifier.GenerateEquity(string.Empty, _market, mapSymbol: false), string.Empty); - break; - case SecurityType.IndexOption: - underlying = Symbol.Create(string.Empty, SecurityType.Index, _market); - break; - default: - throw new NotSupportedException($"ChainSymbolProvider.CreateChainsRequestSymbol(): " + - $"unsupported security type {_securityType}"); - } - - return Symbol.CreateCanonicalOption(underlying); - } - - /// - /// Gets all the available symbols keyed by the canonical symbol from the available price data in the data folder. - /// - private Dictionary> GetSymbolsFromDataFiles() { var result = new Dictionary>(); @@ -241,7 +178,7 @@ private List GetSymbolsFromZipEntryNames(string zipFileName, Symbol cano /// /// Orders the given chain of contracts. /// - private static IEnumerable OrderSymbols(IEnumerable symbols, SecurityType securityType) + protected static IEnumerable OrderSymbols(IEnumerable symbols, SecurityType securityType) { if (securityType.IsOption()) { diff --git a/Lean.DataSource.DerivativeUniverseGenerator/DerivativeUniverseGenerator.cs b/Lean.DataSource.DerivativeUniverseGenerator/DerivativeUniverseGenerator.cs index b458ac9..57bf51b 100644 --- a/Lean.DataSource.DerivativeUniverseGenerator/DerivativeUniverseGenerator.cs +++ b/Lean.DataSource.DerivativeUniverseGenerator/DerivativeUniverseGenerator.cs @@ -165,11 +165,7 @@ private Dictionary> GetSymbolsToProcess() /// /// Gets the available universe symbols grouped by their canonical symbol. /// - protected virtual Dictionary> GetSymbols() - { - var symbolChainProvider = new ChainSymbolProvider(_dataCacheProvider, _processingDate, _securityType, _market, _dataFolderRoot); - return symbolChainProvider.GetSymbols(); - } + protected abstract Dictionary> GetSymbols(); /// /// Filters the symbols to process based on the given list of symbols. diff --git a/Lean.DataSource.OptionsUniverseGenerator/OptionChainSymbolProvider.cs b/Lean.DataSource.OptionsUniverseGenerator/OptionChainSymbolProvider.cs new file mode 100644 index 0000000..63e5895 --- /dev/null +++ b/Lean.DataSource.OptionsUniverseGenerator/OptionChainSymbolProvider.cs @@ -0,0 +1,104 @@ +/* + * 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.Linq; +using QuantConnect.Util; +using QuantConnect.Interfaces; +using System.Collections.Generic; +using QuantConnect.Configuration; +using QuantConnect.DataSource.DerivativeUniverseGenerator; + +namespace QuantConnect.DataSource.OptionsUniverseGenerator +{ + /// + /// Options chain symbol provider used for fetching the option chains from data file names + /// + public class OptionChainSymbolProvider : ChainSymbolProvider + { + private readonly IOptionChainProvider _optionChainProvider; + private readonly string _market; + + /// + /// Initializes a new instance of the class + /// + public OptionChainSymbolProvider(IDataCacheProvider dataCacheProvider, DateTime processingDate, SecurityType securityType, + string market, string dataFolderRoot) + : base(dataCacheProvider, processingDate, securityType, market, dataFolderRoot) + { + _market = market; + + if (Config.TryGetValue("universe-option-chain-provider", out var optionChainProviderStr) && + !string.IsNullOrEmpty(optionChainProviderStr)) + { + _optionChainProvider = Composer.Instance.GetExportedValueByTypeName(optionChainProviderStr); + } + } + + /// + /// Gets all the available symbols keyed by the canonical symbol from the available price data in the data folder. + /// + public override Dictionary> GetSymbols() + { + if (_optionChainProvider == null) + { + return base.GetSymbols(); + } + + // A tickerless dummy symbol fetches the contracts of every canonical of the + // generator's security type and market the provider finds + var contracts = _optionChainProvider.GetOptionContractList(CreateChainsRequestSymbol(), _processingDate)?.ToList(); + if (contracts == null || contracts.Count == 0) + { + // The custom chain provider failed, fallback to the file-based chains + return base.GetSymbols(); + } + + return contracts + .Where(symbol => symbol.SecurityType == _securityType + && symbol.ID.Market == _market + // do not return expired contracts + && _processingDate.Date < symbol.ID.Date.Date) + .Distinct() + .GroupBy(symbol => symbol.Canonical) + .ToDictionary(group => group.Key, group => OrderSymbols(group, _securityType).ToList()); + } + + /// + /// Creates the tickerless dummy symbol used to request the chains of every canonical of the + /// generator's security type and market from the custom chain provider + /// + private Symbol CreateChainsRequestSymbol() + { + Symbol underlying; + switch (_securityType) + { + case SecurityType.Option: + // equity SID generation must skip mapping, which rejects empty tickers + underlying = new Symbol(SecurityIdentifier.GenerateEquity(string.Empty, _market, mapSymbol: false), string.Empty); + break; + case SecurityType.IndexOption: + underlying = Symbol.Create(string.Empty, SecurityType.Index, _market); + break; + default: + throw new NotSupportedException($"OptionChainSymbolProvider.CreateChainsRequestSymbol(): " + + $"unsupported security type {_securityType}"); + } + + return Symbol.CreateCanonicalOption(underlying); + } + } +} diff --git a/Lean.DataSource.OptionsUniverseGenerator/OptionsUniverseGenerator.cs b/Lean.DataSource.OptionsUniverseGenerator/OptionsUniverseGenerator.cs index 8d92bda..12b2d67 100644 --- a/Lean.DataSource.OptionsUniverseGenerator/OptionsUniverseGenerator.cs +++ b/Lean.DataSource.OptionsUniverseGenerator/OptionsUniverseGenerator.cs @@ -79,6 +79,12 @@ protected override IDerivativeUniverseFileEntry CreateUniverseEntry(Symbol symbo return new OptionUniverseEntry(symbol); } + protected override Dictionary> GetSymbols() + { + var symbolChainProvider = new OptionChainSymbolProvider(_dataCacheProvider, _processingDate, _securityType, _market, _dataFolderRoot); + return symbolChainProvider.GetSymbols(); + } + protected override bool NeedsUnderlyingData() { // We don't need underlying data for future options, since they don't have greeks, so no need for underlying data for calculation diff --git a/QuantConnect.DataSource.DerivativeUniverseGeneratorTests/DerivativeUniverseGeneratorTests.cs b/QuantConnect.DataSource.DerivativeUniverseGeneratorTests/DerivativeUniverseGeneratorTests.cs index 35bd8ca..ba4a1e2 100644 --- a/QuantConnect.DataSource.DerivativeUniverseGeneratorTests/DerivativeUniverseGeneratorTests.cs +++ b/QuantConnect.DataSource.DerivativeUniverseGeneratorTests/DerivativeUniverseGeneratorTests.cs @@ -159,6 +159,11 @@ protected override Dictionary> FilterSymbols(Dictionary> GetSymbols() + { + return new Dictionary>(); + } + protected override IDerivativeUniverseFileEntry CreateUniverseEntry(Symbol symbol) { return new BaseDerivativeUniverseFileEntry(symbol);