Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Algorithm/QCAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -2398,6 +2402,26 @@ public IndexOption AddIndexOptionContract(Symbol symbol, Resolution? resolution
return (IndexOption)AddOptionContract(symbol, resolution, fillForward);
}

/// <summary>
/// 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
/// </summary>
private void WarnIfUnderlyingResolutionIsCoarser(Symbol option, List<SubscriptionDataConfig> 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;
}
}

/// <summary>
/// Creates and adds a new single <see cref="Option"/> contract to the algorithm
/// </summary>
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions Common/Messages/Messages.Algorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")}.";
}

/// <summary>
/// Returns a warning message saying an option uses a finer resolution than its underlying
/// </summary>
[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.";
}
}

/// <summary>
Expand Down
77 changes: 77 additions & 0 deletions Tests/Algorithm/AlgorithmAddDataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading