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)