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