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/3] 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/3] 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) From bbcb0cd6ada0b0dc2c3c67667f9fe35655308fc2 Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Thu, 10 Sep 2026 11:21:11 -0300 Subject: [PATCH 3/3] Warn on coarse underlying resolution from AddOption too Move the check into a shared helper used by both AddOptionContract and the canonical AddOption path, and shorten the warning message. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SNgpU4gukVJCH1j5QNzDcF --- Algorithm/QCAlgorithm.cs | 34 +++++++++++++++++------- Common/Messages/Messages.Algorithm.cs | 8 +++--- Tests/Algorithm/AlgorithmAddDataTests.cs | 30 +++++++++++++++++++-- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs index 75f20e4a9158..e353d0cfb2a2 100644 --- a/Algorithm/QCAlgorithm.cs +++ b/Algorithm/QCAlgorithm.cs @@ -107,7 +107,7 @@ public partial class QCAlgorithm : MarshalByRefObject, IAlgorithm private bool _tagsLimitReachedLogSent; private bool _tagsCollectionTruncatedLogSent; private bool _hasShownDailyConsolidationWarning; - private bool _optionContractUnderlyingResolutionWarningSent; + private bool _optionUnderlyingResolutionWarningSent; private bool _indexOptionTickerAsUnderlyingWarningSent; private DateTime _start; private DateTime _startDate; //Default start and end dates. @@ -2199,6 +2199,9 @@ public Option AddOption(Symbol underlying, string targetOption, Resolution? reso canonicalSymbol = QuantConnect.Symbol.CreateCanonicalOption(underlying, targetOption, market, alias); } + WarnIfUnderlyingResolutionIsCoarser(canonicalSymbol, + SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(underlying), resolution); + return (Option)AddSecurity(canonicalSymbol, resolution, fillForward, leverage); } @@ -2399,6 +2402,26 @@ public IndexOption AddIndexOptionContract(Symbol symbol, Resolution? resolution return (IndexOption)AddOptionContract(symbol, resolution, fillForward); } + /// + /// Warns once if the option resolution is finer than the existing underlying subscription, + /// since the option pricing models would then use a stale underlying price + /// + private void WarnIfUnderlyingResolutionIsCoarser(Symbol option, List underlyingConfigs, Resolution? optionResolution) + { + if (_optionUnderlyingResolutionWarningSent || underlyingConfigs.Count == 0) + { + return; + } + + var resolution = optionResolution ?? UniverseSettings.Resolution; + var underlyingResolution = underlyingConfigs.GetHighestResolution(); + if (underlyingResolution > resolution) + { + Debug($"Warning: {Messages.QCAlgorithm.OptionUnderlyingResolutionIsCoarser(option, resolution, underlyingResolution)}"); + _optionUnderlyingResolutionWarningSent = true; + } + } + /// /// Creates and adds a new single contract to the algorithm /// @@ -2455,14 +2478,7 @@ public Option AddOptionContract(Symbol symbol, Resolution? resolution = null, bo } } - var optionResolution = resolution ?? UniverseSettings.Resolution; - var underlyingResolution = underlyingConfigs.GetHighestResolution(); - if (underlyingResolution > optionResolution && !_optionContractUnderlyingResolutionWarningSent) - { - Debug($"Warning: {Messages.QCAlgorithm.AddOptionContractUnderlyingResolution( - symbol, optionResolution, underlying, underlyingResolution)}"); - _optionContractUnderlyingResolutionWarningSent = true; - } + WarnIfUnderlyingResolutionIsCoarser(symbol, underlyingConfigs, resolution); var configs = SubscriptionManager.SubscriptionDataConfigService.Add(symbol, resolution, fillForward, extendedMarketHours, dataNormalizationMode: DataNormalizationMode.Raw); diff --git a/Common/Messages/Messages.Algorithm.cs b/Common/Messages/Messages.Algorithm.cs index a5a13abd79a9..4c9bb8ddffe0 100644 --- a/Common/Messages/Messages.Algorithm.cs +++ b/Common/Messages/Messages.Algorithm.cs @@ -104,12 +104,10 @@ public static string AddDataInvalidPyObjectType(string repr) /// 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, - global::QuantConnect.Symbol underlying, Resolution underlyingResolution) + public static string OptionUnderlyingResolutionIsCoarser(global::QuantConnect.Symbol option, Resolution optionResolution, 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."; + return $"Option {option} uses {optionResolution} resolution but its underlying {option.Underlying} uses {underlyingResolution}, " + + $"so Greeks and implied volatility will use stale prices. Add the underlying at {optionResolution} resolution or finer."; } } diff --git a/Tests/Algorithm/AlgorithmAddDataTests.cs b/Tests/Algorithm/AlgorithmAddDataTests.cs index 49b904627af2..f9e480b943cc 100644 --- a/Tests/Algorithm/AlgorithmAddDataTests.cs +++ b/Tests/Algorithm/AlgorithmAddDataTests.cs @@ -738,7 +738,7 @@ public void AddOptionContractWarnsForCoarseUnderlyingResolution( Assert.DoesNotThrow(() => algorithm.AddOptionContract(option, optionResolution)); - var warnings = algorithm.DebugMessages.Where(message => message.Contains("finer than its underlying")).ToList(); + var warnings = algorithm.DebugMessages.Where(message => message.Contains("but its underlying")).ToList(); Assert.AreEqual(shouldWarn ? 1 : 0, warnings.Count); if (shouldWarn) { @@ -756,6 +756,32 @@ public void AddOptionContractUsesHighestAvailableUnderlyingResolution() 100m, new DateTime(2027, 1, 15)); Assert.DoesNotThrow(() => algorithm.AddOptionContract(option, Resolution.Minute)); + + Assert.IsFalse(algorithm.DebugMessages.Any(message => message.Contains("but its underlying"))); + } + + [TestCase(Resolution.Daily, Resolution.Minute, true)] + [TestCase(Resolution.Minute, Resolution.Minute, false)] + [TestCase(Resolution.Second, Resolution.Minute, false)] + public void AddOptionWarnsForCoarseUnderlyingResolution( + Resolution underlyingResolution, Resolution optionResolution, bool shouldWarn) + { + var algorithm = Algorithm(); + algorithm.AddEquity("SPY", underlyingResolution); + + Assert.DoesNotThrow(() => algorithm.AddOption("SPY", optionResolution)); + + Assert.AreEqual(shouldWarn ? 1 : 0, algorithm.DebugMessages.Count(message => message.Contains("but its underlying"))); + } + + [Test] + public void AddOptionDoesNotWarnWhenUnderlyingIsNotPresent() + { + var algorithm = Algorithm(); + + Assert.DoesNotThrow(() => algorithm.AddOption("SPY", Resolution.Minute)); + + Assert.IsFalse(algorithm.DebugMessages.Any(message => message.Contains("but its underlying"))); } [Test] @@ -772,7 +798,7 @@ public void AddOptionContractWarnsOnceForCoarseUnderlyingResolution() Assert.DoesNotThrow(() => algorithm.AddOptionContract(firstOption)); Assert.DoesNotThrow(() => algorithm.AddOptionContract(secondOption)); - Assert.AreEqual(1, algorithm.DebugMessages.Count(message => message.Contains("finer than its underlying"))); + Assert.AreEqual(1, algorithm.DebugMessages.Count(message => message.Contains("but its underlying"))); } private static SubscriptionDataConfig GetMatchingSubscription(QCAlgorithm algorithm, Symbol symbol, Type type)