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] 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