Skip to content
Closed
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
8 changes: 8 additions & 0 deletions Algorithm/QCAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions Common/Messages/Messages.Algorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")}.";
}

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

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