diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs
index daf195a7a1e1..e353d0cfb2a2 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 _optionUnderlyingResolutionWarningSent;
private bool _indexOptionTickerAsUnderlyingWarningSent;
private DateTime _start;
private DateTime _startDate; //Default start and end dates.
@@ -2198,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);
}
@@ -2398,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
///
@@ -2454,6 +2478,8 @@ public Option AddOptionContract(Symbol symbol, Resolution? resolution = null, bo
}
}
+ WarnIfUnderlyingResolutionIsCoarser(symbol, underlyingConfigs, resolution);
+
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..4c9bb8ddffe0 100644
--- a/Common/Messages/Messages.Algorithm.cs
+++ b/Common/Messages/Messages.Algorithm.cs
@@ -99,6 +99,16 @@ 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 warning message saying an option uses a finer resolution than its underlying
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string OptionUnderlyingResolutionIsCoarser(global::QuantConnect.Symbol option, Resolution optionResolution, Resolution underlyingResolution)
+ {
+ 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 d4834147b4aa..f9e480b943cc 100644
--- a/Tests/Algorithm/AlgorithmAddDataTests.cs
+++ b/Tests/Algorithm/AlgorithmAddDataTests.cs
@@ -724,6 +724,83 @@ 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 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));
+
+ Assert.DoesNotThrow(() => algorithm.AddOptionContract(option, optionResolution));
+
+ var warnings = algorithm.DebugMessages.Where(message => message.Contains("but its underlying")).ToList();
+ Assert.AreEqual(shouldWarn ? 1 : 0, warnings.Count);
+ if (shouldWarn)
+ {
+ StringAssert.Contains($"Add the underlying at {optionResolution} resolution or finer", warnings.Single());
+ }
+ }
+
+ [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));
+
+ 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]
+ public void AddOptionContractWarnsOnceForCoarseUnderlyingResolution()
+ {
+ var algorithm = Algorithm();
+ algorithm.UniverseSettings.Resolution = Resolution.Minute;
+ var underlying = algorithm.AddEquity("SPY", Resolution.Daily).Symbol;
+ 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.AreEqual(1, algorithm.DebugMessages.Count(message => message.Contains("but its underlying")));
+ }
+
private static SubscriptionDataConfig GetMatchingSubscription(QCAlgorithm algorithm, Symbol symbol, Type type)
{
// find a subscription matchin the requested type with a higher resolution than requested