diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs index daf195a7a1e1..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. @@ -2454,6 +2455,15 @@ 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; + } + 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..a5a13abd79a9 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 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) + { + 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..49b904627af2 100644 --- a/Tests/Algorithm/AlgorithmAddDataTests.cs +++ b/Tests/Algorithm/AlgorithmAddDataTests.cs @@ -724,6 +724,57 @@ 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("finer than 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)); + } + + [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("finer than 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