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)