From d304d750e353495b94e2aef00968dbbd045c580e Mon Sep 17 00:00:00 2001 From: Pengyi Peng <74917296+pengpengyi92@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:08:27 +0800 Subject: [PATCH 1/2] Validate option underlying subscription resolution --- Algorithm/QCAlgorithm.cs | 8 ++++ Common/Messages/Messages.Algorithm.cs | 12 ++++++ Tests/Algorithm/AlgorithmAddDataTests.cs | 48 ++++++++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs index daf195a7a1e1..1d38fd7d3714 100644 --- a/Algorithm/QCAlgorithm.cs +++ b/Algorithm/QCAlgorithm.cs @@ -2454,6 +2454,14 @@ public Option AddOptionContract(Symbol symbol, Resolution? resolution = null, bo } } + var optionResolution = resolution ?? UniverseSettings.Resolution; + var underlyingResolution = underlyingConfigs.GetHighestResolution(); + if (underlyingResolution > optionResolution) + { + throw new ArgumentException(Messages.QCAlgorithm.AddOptionContractUnderlyingResolution( + symbol, optionResolution, underlying, underlyingResolution)); + } + var configs = SubscriptionManager.SubscriptionDataConfigService.Add(symbol, resolution, fillForward, extendedMarketHours, dataNormalizationMode: DataNormalizationMode.Raw); var option = (Option)Securities.CreateSecurity(symbol, configs, leverage, underlying: underlyingSecurity); diff --git a/Common/Messages/Messages.Algorithm.cs b/Common/Messages/Messages.Algorithm.cs index 288a0197cbcc..7784b27b12d8 100644 --- a/Common/Messages/Messages.Algorithm.cs +++ b/Common/Messages/Messages.Algorithm.cs @@ -99,6 +99,18 @@ public static string AddDataInvalidPyObjectType(string repr) return $"{AlgorithmPrefix()}.{FormatCode("AddData")}(): the first argument must be a custom data type (a Python class deriving from {FormatCode("PythonData")} or a CLR {FormatCode("BaseData")} type), but received {repr}. " + $"To subscribe to built-in asset classes use, for example, {FormatCode("AddEquity")} or {FormatCode("AddCrypto")}."; } + + /// + /// Returns a string message saying an option cannot use a finer resolution than its underlying + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string AddOptionContractUnderlyingResolution(global::QuantConnect.Symbol option, Resolution optionResolution, + global::QuantConnect.Symbol underlying, Resolution underlyingResolution) + { + return $"{AlgorithmPrefix()}.{FormatCode("AddOptionContract")}(): option contract {option} uses {optionResolution} resolution, " + + $"which is finer than its underlying {underlying} subscription at {underlyingResolution} resolution. " + + $"Add the underlying at {optionResolution} resolution or finer before adding the option contract so its implied volatility and Greeks use a current underlying price."; + } } /// diff --git a/Tests/Algorithm/AlgorithmAddDataTests.cs b/Tests/Algorithm/AlgorithmAddDataTests.cs index d4834147b4aa..4e6d0fbadad7 100644 --- a/Tests/Algorithm/AlgorithmAddDataTests.cs +++ b/Tests/Algorithm/AlgorithmAddDataTests.cs @@ -724,6 +724,54 @@ public void AddOptionContractWithDelistedUnderlyingThrows(SecurityType underlyin Assert.IsTrue(exception.Message.Contains("is delisted"), $"Unexpected exception message: {exception.Message}"); } + [TestCase(Resolution.Daily, Resolution.Minute, true)] + [TestCase(Resolution.Hour, Resolution.Minute, true)] + [TestCase(Resolution.Minute, Resolution.Minute, false)] + [TestCase(Resolution.Second, Resolution.Minute, false)] + public void AddOptionContractValidatesUnderlyingResolution( + Resolution underlyingResolution, Resolution optionResolution, bool shouldThrow) + { + var algorithm = Algorithm(); + var underlying = algorithm.AddEquity("SPY", underlyingResolution).Symbol; + var option = Symbol.CreateOption(underlying, Market.USA, OptionStyle.American, OptionRight.Call, + 100m, new DateTime(2027, 1, 15)); + + if (shouldThrow) + { + var exception = Assert.Throws(() => algorithm.AddOptionContract(option, optionResolution)); + StringAssert.Contains("finer than its underlying", exception.Message); + StringAssert.Contains($"Add the underlying at {optionResolution} resolution or finer", exception.Message); + } + else + { + Assert.DoesNotThrow(() => algorithm.AddOptionContract(option, optionResolution)); + } + } + + [Test] + public void AddOptionContractUsesHighestAvailableUnderlyingResolution() + { + var algorithm = Algorithm(); + var underlying = algorithm.AddEquity("SPY", Resolution.Daily).Symbol; + algorithm.AddEquity("SPY", Resolution.Minute); + var option = Symbol.CreateOption(underlying, Market.USA, OptionStyle.American, OptionRight.Call, + 100m, new DateTime(2027, 1, 15)); + + Assert.DoesNotThrow(() => algorithm.AddOptionContract(option, Resolution.Minute)); + } + + [Test] + public void AddOptionContractValidatesUnderlyingResolutionFromUniverseSettings() + { + var algorithm = Algorithm(); + algorithm.UniverseSettings.Resolution = Resolution.Minute; + var underlying = algorithm.AddEquity("SPY", Resolution.Daily).Symbol; + var option = Symbol.CreateOption(underlying, Market.USA, OptionStyle.American, OptionRight.Call, + 100m, new DateTime(2027, 1, 15)); + + Assert.Throws(() => algorithm.AddOptionContract(option)); + } + private static SubscriptionDataConfig GetMatchingSubscription(QCAlgorithm algorithm, Symbol symbol, Type type) { // find a subscription matchin the requested type with a higher resolution than requested From 47ad3c08040b648c497f590bf0f5bcf4a74590c8 Mon Sep 17 00:00:00 2001 From: Pengyi Peng <74917296+pengpengyi92@users.noreply.github.com> Date: Fri, 28 Aug 2026 02:01:46 +0800 Subject: [PATCH 2/2] Warn once for coarse option underlying resolution --- Algorithm/QCAlgorithm.cs | 8 ++++--- Common/Messages/Messages.Algorithm.cs | 2 +- Tests/Algorithm/AlgorithmAddDataTests.cs | 29 +++++++++++++----------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs index 1d38fd7d3714..75f20e4a9158 100644 --- a/Algorithm/QCAlgorithm.cs +++ b/Algorithm/QCAlgorithm.cs @@ -107,6 +107,7 @@ public partial class QCAlgorithm : MarshalByRefObject, IAlgorithm private bool _tagsLimitReachedLogSent; private bool _tagsCollectionTruncatedLogSent; private bool _hasShownDailyConsolidationWarning; + private bool _optionContractUnderlyingResolutionWarningSent; private bool _indexOptionTickerAsUnderlyingWarningSent; private DateTime _start; private DateTime _startDate; //Default start and end dates. @@ -2456,10 +2457,11 @@ public Option AddOptionContract(Symbol symbol, Resolution? resolution = null, bo var optionResolution = resolution ?? UniverseSettings.Resolution; var underlyingResolution = underlyingConfigs.GetHighestResolution(); - if (underlyingResolution > optionResolution) + if (underlyingResolution > optionResolution && !_optionContractUnderlyingResolutionWarningSent) { - throw new ArgumentException(Messages.QCAlgorithm.AddOptionContractUnderlyingResolution( - symbol, optionResolution, underlying, underlyingResolution)); + Debug($"Warning: {Messages.QCAlgorithm.AddOptionContractUnderlyingResolution( + symbol, optionResolution, underlying, underlyingResolution)}"); + _optionContractUnderlyingResolutionWarningSent = true; } var configs = SubscriptionManager.SubscriptionDataConfigService.Add(symbol, resolution, fillForward, extendedMarketHours, diff --git a/Common/Messages/Messages.Algorithm.cs b/Common/Messages/Messages.Algorithm.cs index 7784b27b12d8..a5a13abd79a9 100644 --- a/Common/Messages/Messages.Algorithm.cs +++ b/Common/Messages/Messages.Algorithm.cs @@ -101,7 +101,7 @@ public static string AddDataInvalidPyObjectType(string repr) } /// - /// Returns a string message saying an option cannot use a finer resolution than its underlying + /// Returns a warning message saying an option uses a finer resolution than its underlying /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string AddOptionContractUnderlyingResolution(global::QuantConnect.Symbol option, Resolution optionResolution, diff --git a/Tests/Algorithm/AlgorithmAddDataTests.cs b/Tests/Algorithm/AlgorithmAddDataTests.cs index 4e6d0fbadad7..49b904627af2 100644 --- a/Tests/Algorithm/AlgorithmAddDataTests.cs +++ b/Tests/Algorithm/AlgorithmAddDataTests.cs @@ -728,23 +728,21 @@ public void AddOptionContractWithDelistedUnderlyingThrows(SecurityType underlyin [TestCase(Resolution.Hour, Resolution.Minute, true)] [TestCase(Resolution.Minute, Resolution.Minute, false)] [TestCase(Resolution.Second, Resolution.Minute, false)] - public void AddOptionContractValidatesUnderlyingResolution( - Resolution underlyingResolution, Resolution optionResolution, bool shouldThrow) + public void AddOptionContractWarnsForCoarseUnderlyingResolution( + Resolution underlyingResolution, Resolution optionResolution, bool shouldWarn) { var algorithm = Algorithm(); var underlying = algorithm.AddEquity("SPY", underlyingResolution).Symbol; var option = Symbol.CreateOption(underlying, Market.USA, OptionStyle.American, OptionRight.Call, 100m, new DateTime(2027, 1, 15)); - if (shouldThrow) - { - var exception = Assert.Throws(() => algorithm.AddOptionContract(option, optionResolution)); - StringAssert.Contains("finer than its underlying", exception.Message); - StringAssert.Contains($"Add the underlying at {optionResolution} resolution or finer", exception.Message); - } - else + Assert.DoesNotThrow(() => algorithm.AddOptionContract(option, optionResolution)); + + var warnings = algorithm.DebugMessages.Where(message => message.Contains("finer than its underlying")).ToList(); + Assert.AreEqual(shouldWarn ? 1 : 0, warnings.Count); + if (shouldWarn) { - Assert.DoesNotThrow(() => algorithm.AddOptionContract(option, optionResolution)); + StringAssert.Contains($"Add the underlying at {optionResolution} resolution or finer", warnings.Single()); } } @@ -761,15 +759,20 @@ public void AddOptionContractUsesHighestAvailableUnderlyingResolution() } [Test] - public void AddOptionContractValidatesUnderlyingResolutionFromUniverseSettings() + public void AddOptionContractWarnsOnceForCoarseUnderlyingResolution() { var algorithm = Algorithm(); algorithm.UniverseSettings.Resolution = Resolution.Minute; var underlying = algorithm.AddEquity("SPY", Resolution.Daily).Symbol; - var option = Symbol.CreateOption(underlying, Market.USA, OptionStyle.American, OptionRight.Call, + var firstOption = Symbol.CreateOption(underlying, Market.USA, OptionStyle.American, OptionRight.Call, 100m, new DateTime(2027, 1, 15)); + var secondOption = Symbol.CreateOption(underlying, Market.USA, OptionStyle.American, OptionRight.Put, + 105m, new DateTime(2027, 1, 15)); + + Assert.DoesNotThrow(() => algorithm.AddOptionContract(firstOption)); + Assert.DoesNotThrow(() => algorithm.AddOptionContract(secondOption)); - Assert.Throws(() => algorithm.AddOptionContract(option)); + Assert.AreEqual(1, algorithm.DebugMessages.Count(message => message.Contains("finer than its underlying"))); } private static SubscriptionDataConfig GetMatchingSubscription(QCAlgorithm algorithm, Symbol symbol, Type type)