From fc31b529646cf9cb2114904f89aa6160183d2c94 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 12 Dec 2024 17:33:27 -0400 Subject: [PATCH 1/7] Add helper method to calculate options expiration date time This allows to compute accurate time till expiry for greek indicators to be able to calculate on the actual expiration date before market close --- Common/Securities/Option/OptionSymbol.cs | 47 +++++++++++++++---- Indicators/ImpliedVolatility.cs | 3 +- Indicators/OptionGreekIndicatorBase.cs | 4 +- .../Securities/Options/OptionSymbolTests.cs | 26 ++++++++++ Tests/Indicators/DeltaTests.cs | 27 +++++++++++ Tests/Indicators/GammaTests.cs | 35 ++++++++++++++ Tests/Indicators/ImpliedVolatilityTests.cs | 25 ++++++++++ Tests/Indicators/RhoTests.cs | 27 +++++++++++ Tests/Indicators/ThetaTests.cs | 27 +++++++++++ Tests/Indicators/VegaTests.cs | 27 +++++++++++ 10 files changed, 237 insertions(+), 11 deletions(-) diff --git a/Common/Securities/Option/OptionSymbol.cs b/Common/Securities/Option/OptionSymbol.cs index 6842a52ff9ec..77653316e59b 100644 --- a/Common/Securities/Option/OptionSymbol.cs +++ b/Common/Securities/Option/OptionSymbol.cs @@ -120,6 +120,21 @@ public static DateTime GetLastDayOfTrading(Symbol symbol) return symbolDateTime.AddDays(daysBefore).Date; } + /// + /// Returns the actual expiration date time, adjusted to market close of the expiration day. + /// + /// The option contract symbol + /// The expiration date time, adjusted to market close of the expiration day + public static DateTime GetExpirationDateTime(Symbol symbol) + { + if (!TryGetExpirationDateTime(symbol, out var expiryTime, out _)) + { + throw new ArgumentException("The symbol must be an option type"); + } + + return expiryTime; + } + /// /// Returns true if the option contract is expired at the specified time /// @@ -127,24 +142,34 @@ public static DateTime GetLastDayOfTrading(Symbol symbol) /// The current time (UTC) /// True if the option contract is expired at the specified time, false otherwise public static bool IsOptionContractExpired(Symbol symbol, DateTime currentTimeUtc) + { + if (TryGetExpirationDateTime(symbol, out var expiryTime, out var exchangeHours)) + { + var currentTime = currentTimeUtc.ConvertFromUtc(exchangeHours.TimeZone); + return currentTime >= expiryTime; + } + + return false; + } + + private static bool TryGetExpirationDateTime(Symbol symbol, out DateTime expiryTime, out SecurityExchangeHours exchangeHours) { if (!symbol.SecurityType.IsOption()) { + expiryTime = default; + exchangeHours = null; return false; } - var exchangeHours = MarketHoursDatabase.FromDataFolder() - .GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType); - - var currentTime = currentTimeUtc.ConvertFromUtc(exchangeHours.TimeZone); + exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType); - // Ideally we can calculate expiry on the date of the symbol ID, but if that exchange is not open on that day we + // Ideally we can calculate expiry on the date of the symbol ID, but if that exchange is not open on that day we // will consider expired on the last trading day close before this; Example in AddOptionContractExpiresRegressionAlgorithm - var expiryDay = exchangeHours.IsDateOpen(symbol.ID.Date) + var lastTradingDay = exchangeHours.IsDateOpen(symbol.ID.Date) ? symbol.ID.Date : exchangeHours.GetPreviousTradingDay(symbol.ID.Date); - var expiryTime = exchangeHours.GetNextMarketClose(expiryDay, false); + expiryTime = exchangeHours.GetNextMarketClose(lastTradingDay, false); // Once bug 6189 was solved in ´GetNextMarketClose()´ there was found possible bugs on some futures symbol.ID.Date or delisting/liquidation handle event. // Specifically see 'DelistingFutureOptionRegressionAlgorithm' where Symbol.ID.Date: 4/1/2012 00:00 ExpiryTime: 4/2/2012 16:00 for Milk 3 futures options. @@ -163,7 +188,13 @@ public static bool IsOptionContractExpired(Symbol symbol, DateTime currentTimeUt expiryTime = symbol.ID.Date.AddDays(1).Date; } - return currentTime >= expiryTime; + // Standard index options are AM-settled, which means they settle the morning after the last trading day + if (symbol.SecurityType == SecurityType.IndexOption && IsStandard(symbol)) + { + expiryTime = exchangeHours.GetNextMarketOpen(expiryTime, false); + } + + return true; } } } diff --git a/Indicators/ImpliedVolatility.cs b/Indicators/ImpliedVolatility.cs index db35d023ddfe..c3eb788cba18 100644 --- a/Indicators/ImpliedVolatility.cs +++ b/Indicators/ImpliedVolatility.cs @@ -265,7 +265,8 @@ protected override decimal Calculate(IndicatorDataPoint input) RiskFreeRate.Update(time, _riskFreeInterestRateModel.GetInterestRate(time)); DividendYield.Update(time, _dividendYieldModel.GetDividendYield(time, UnderlyingPrice.Current.Value)); - var timeTillExpiry = Convert.ToDecimal(OptionGreekIndicatorsHelper.TimeTillExpiry(Expiry, time)); + var timeTillExpiry = Convert.ToDecimal( + OptionGreekIndicatorsHelper.TimeTillExpiry(Securities.Option.OptionSymbol.GetExpirationDateTime(OptionSymbol), time)); _impliedVolatility = CalculateIV(timeTillExpiry); } diff --git a/Indicators/OptionGreekIndicatorBase.cs b/Indicators/OptionGreekIndicatorBase.cs index ae166efa0c23..edbc04c08954 100644 --- a/Indicators/OptionGreekIndicatorBase.cs +++ b/Indicators/OptionGreekIndicatorBase.cs @@ -16,7 +16,6 @@ using System; using Python.Runtime; using QuantConnect.Data; -using QuantConnect.Logging; using QuantConnect.Python; namespace QuantConnect.Indicators @@ -184,7 +183,8 @@ protected override decimal Calculate(IndicatorDataPoint input) RiskFreeRate.Update(time, _riskFreeInterestRateModel.GetInterestRate(time)); DividendYield.Update(time, _dividendYieldModel.GetDividendYield(time, UnderlyingPrice.Current.Value)); - var timeTillExpiry = Convert.ToDecimal(OptionGreekIndicatorsHelper.TimeTillExpiry(Expiry, time)); + var timeTillExpiry = Convert.ToDecimal( + OptionGreekIndicatorsHelper.TimeTillExpiry(Securities.Option.OptionSymbol.GetExpirationDateTime(OptionSymbol), time)); try { _greekValue = timeTillExpiry < 0 ? 0 : CalculateGreek(timeTillExpiry); diff --git a/Tests/Common/Securities/Options/OptionSymbolTests.cs b/Tests/Common/Securities/Options/OptionSymbolTests.cs index f3df9446505a..03f5e1a98481 100644 --- a/Tests/Common/Securities/Options/OptionSymbolTests.cs +++ b/Tests/Common/Securities/Options/OptionSymbolTests.cs @@ -14,6 +14,7 @@ */ using System; +using System.Collections.Generic; using NUnit.Framework; using QuantConnect.Securities.Option; @@ -69,5 +70,30 @@ public void IsOptionContractExpiredReturnsFalseIfTimeOfDayDiffer() Assert.IsFalse(OptionSymbol.IsOptionContractExpired(symbol, new DateTime(2022, 03, 11))); } + + private static IEnumerable ExpirationDateTimeTestCases() + { + var equityOption = Symbols.SPY_C_192_Feb19_2016; + yield return new TestCaseData(equityOption, new DateTime(2016, 02, 19, 16, 0, 0)); + + // Expires on a Saturday, so the expiration date time should be the Friday before + equityOption = Symbols.CreateOptionSymbol("SPY", OptionRight.Call, 192m, new DateTime(2016, 02, 20)); + yield return new TestCaseData(equityOption, new DateTime(2016, 02, 19, 16, 0, 0)); + + var pmSettledIndexOption = Symbol.CreateOption(Symbols.SPX, "SPXW", Market.USA, OptionStyle.European, + OptionRight.Call, 200m, new DateTime(2016, 02, 12)); + yield return new TestCaseData(pmSettledIndexOption, new DateTime(2016, 02, 12, 15, 15, 0)); + + var amSettledIndexOption = Symbol.CreateOption(Symbols.SPX, "SPX", Market.USA, OptionStyle.European, + OptionRight.Call, 200m, new DateTime(2016, 02, 18)); + yield return new TestCaseData(amSettledIndexOption, new DateTime(2016, 02, 19, 8, 30, 0)); + } + + [TestCaseSource(nameof(ExpirationDateTimeTestCases))] + public void CalculatesExpirationDateTime(Symbol symbol, DateTime expectedExpirationDateTime) + { + var expirationDateTime = OptionSymbol.GetExpirationDateTime(symbol); + Assert.AreEqual(expectedExpirationDateTime, expirationDateTime); + } } } diff --git a/Tests/Indicators/DeltaTests.cs b/Tests/Indicators/DeltaTests.cs index a12fa1c31247..f5e3025ce4f8 100644 --- a/Tests/Indicators/DeltaTests.cs +++ b/Tests/Indicators/DeltaTests.cs @@ -17,6 +17,7 @@ using QuantConnect.Algorithm; using QuantConnect.Data; using QuantConnect.Indicators; +using System; using System.IO; using System.Linq; @@ -126,5 +127,31 @@ public void ComparesAgainstExternalData2(decimal price, decimal spotPrice, Optio Assert.AreEqual(refDelta, (double)indicator.Current.Value, 0.0005d); } + + [TestCase(0.5, 470.0, OptionRight.Put, 0)] + [TestCase(0.5, 470.0, OptionRight.Put, 5)] + [TestCase(0.5, 470.0, OptionRight.Put, 10)] + [TestCase(0.5, 470.0, OptionRight.Put, 15)] + [TestCase(15, 450.0, OptionRight.Call, 0)] + [TestCase(15, 450.0, OptionRight.Call, 5)] + [TestCase(15, 450.0, OptionRight.Call, 10)] + [TestCase(0.5, 450.0, OptionRight.Call, 15)] + public void CanComputeOnExpirationDate(decimal price, decimal spotPrice, OptionRight right, int hoursAfterExpiryDate) + { + var expiration = new DateTime(2024, 12, 6); + var symbol = Symbol.CreateOption("SPY", Market.USA, OptionStyle.American, right, 450m, expiration); + var indicator = new Delta(symbol, 0.0403m, 0.0m, + optionModel: OptionPricingModelType.BinomialCoxRossRubinstein, ivModel: OptionPricingModelType.BlackScholes); + + var currentTime = expiration.AddHours(hoursAfterExpiryDate); + + var optionDataPoint = new IndicatorDataPoint(symbol, currentTime, price); + var spotDataPoint = new IndicatorDataPoint(symbol.Underlying, currentTime, spotPrice); + + Assert.IsFalse(indicator.Update(optionDataPoint)); + Assert.IsTrue(indicator.Update(spotDataPoint)); + + Assert.AreNotEqual(0, indicator.Current.Value); + } } } diff --git a/Tests/Indicators/GammaTests.cs b/Tests/Indicators/GammaTests.cs index f85c2b3aa4cb..b0b889203405 100644 --- a/Tests/Indicators/GammaTests.cs +++ b/Tests/Indicators/GammaTests.cs @@ -17,6 +17,7 @@ using QuantConnect.Algorithm; using QuantConnect.Data; using QuantConnect.Indicators; +using System; using System.IO; using System.Linq; @@ -126,5 +127,39 @@ public void ComparesAgainstExternalData2(decimal price, decimal spotPrice, Optio Assert.AreEqual(refGamma, (double)indicator.Current.Value, 0.0005d); } + + [TestCase(0.5, 470.0, OptionRight.Put, OptionStyle.American, 0)] + [TestCase(0.5, 470.0, OptionRight.Put, OptionStyle.American, 5)] + [TestCase(0.5, 470.0, OptionRight.Put, OptionStyle.American, 10)] + [TestCase(0.5, 470.0, OptionRight.Put, OptionStyle.American, 15)] // Expires at 16:00 + [TestCase(15.0, 450.0, OptionRight.Call, OptionStyle.American, 0)] + [TestCase(15.0, 450.0, OptionRight.Call, OptionStyle.American, 5)] + [TestCase(15.0, 450.0, OptionRight.Call, OptionStyle.American, 10)] + [TestCase(15.0, 450.0, OptionRight.Call, OptionStyle.American, 15)] + [TestCase(0.5, 470.0, OptionRight.Put, OptionStyle.European, 0)] + [TestCase(0.5, 470.0, OptionRight.Put, OptionStyle.European, 5)] + [TestCase(0.5, 470.0, OptionRight.Put, OptionStyle.European, 10)] + [TestCase(0.5, 470.0, OptionRight.Put, OptionStyle.European, 15)] + [TestCase(0.5, 450.0, OptionRight.Call, OptionStyle.European, 0)] + [TestCase(0.5, 450.0, OptionRight.Call, OptionStyle.European, 5)] + [TestCase(0.5, 450.0, OptionRight.Call, OptionStyle.European, 10)] + [TestCase(0.5, 450.0, OptionRight.Call, OptionStyle.European, 15)] + public void CanComputeOnExpirationDate(decimal price, decimal spotPrice, OptionRight right, OptionStyle style, int hoursAfterExpiryDate) + { + var expiration = new DateTime(2024, 12, 6); + var symbol = Symbol.CreateOption("SPY", Market.USA, style, right, 450m, expiration); + var model = style == OptionStyle.European ? OptionPricingModelType.BlackScholes : OptionPricingModelType.BinomialCoxRossRubinstein; + var indicator = new Gamma(symbol, 0.0403m, 0.0m, optionModel: model, ivModel: OptionPricingModelType.BlackScholes); + + var currentTime = expiration.AddHours(hoursAfterExpiryDate); + + var optionDataPoint = new IndicatorDataPoint(symbol, currentTime, price); + var spotDataPoint = new IndicatorDataPoint(symbol.Underlying, currentTime, spotPrice); + + Assert.IsFalse(indicator.Update(optionDataPoint)); + Assert.IsTrue(indicator.Update(spotDataPoint)); + + Assert.AreNotEqual(0, indicator.Current.Value); + } } } diff --git a/Tests/Indicators/ImpliedVolatilityTests.cs b/Tests/Indicators/ImpliedVolatilityTests.cs index 1686cd76f381..f24a25757782 100644 --- a/Tests/Indicators/ImpliedVolatilityTests.cs +++ b/Tests/Indicators/ImpliedVolatilityTests.cs @@ -183,6 +183,31 @@ public void ComparesAgainstExternalData2(decimal price, decimal spotPrice, Optio Assert.AreEqual(refIV, (double)indicator.Current.Value, 0.001d); } + [TestCase(0.5, 470.0, OptionRight.Put, 0)] + [TestCase(0.5, 470.0, OptionRight.Put, 5)] + [TestCase(0.5, 470.0, OptionRight.Put, 10)] + [TestCase(0.5, 470.0, OptionRight.Put, 15)] + [TestCase(15, 450.0, OptionRight.Call, 0)] + [TestCase(15, 450.0, OptionRight.Call, 5)] + [TestCase(15, 450.0, OptionRight.Call, 10)] + [TestCase(0.5, 450.0, OptionRight.Call, 15)] + public void CanComputeOnExpirationDate(decimal price, decimal spotPrice, OptionRight right, int hoursAfterExpiryDate) + { + var expiration = new DateTime(2024, 12, 6); + var symbol = Symbol.CreateOption("SPY", Market.USA, OptionStyle.American, right, 450m, expiration); + var indicator = new ImpliedVolatility(symbol, 0.0530m, 0.0153m, optionModel: OptionPricingModelType.BlackScholes); + + var currentTime = expiration.AddHours(hoursAfterExpiryDate); + + var optionDataPoint = new IndicatorDataPoint(symbol, currentTime, price); + var spotDataPoint = new IndicatorDataPoint(symbol.Underlying, currentTime, spotPrice); + + Assert.IsFalse(indicator.Update(optionDataPoint)); + Assert.IsTrue(indicator.Update(spotDataPoint)); + + Assert.AreNotEqual(0, indicator.Current.Value); + } + [Test] public override void WarmsUpProperly() { diff --git a/Tests/Indicators/RhoTests.cs b/Tests/Indicators/RhoTests.cs index b6f547bc9daa..5436439093ce 100644 --- a/Tests/Indicators/RhoTests.cs +++ b/Tests/Indicators/RhoTests.cs @@ -17,6 +17,7 @@ using QuantConnect.Algorithm; using QuantConnect.Data; using QuantConnect.Indicators; +using System; namespace QuantConnect.Tests.Indicators { @@ -83,5 +84,31 @@ public void ComparesAgainstExternalData2(decimal price, decimal spotPrice, Optio Assert.AreEqual(refRho, (double)indicator.Current.Value, 0.0011d); } + + [TestCase(0.5, 470.0, OptionRight.Put, 0)] + [TestCase(0.5, 470.0, OptionRight.Put, 5)] + [TestCase(0.5, 470.0, OptionRight.Put, 10)] + [TestCase(0.5, 470.0, OptionRight.Put, 15)] + [TestCase(15.0, 450.0, OptionRight.Call, 0)] + [TestCase(15.0, 450.0, OptionRight.Call, 5)] + [TestCase(15.0, 450.0, OptionRight.Call, 10)] + [TestCase(0.5, 450.0, OptionRight.Call, 15)] + public void CanComputeOnExpirationDate(decimal price, decimal spotPrice, OptionRight right, int hoursAfterExpiryDate) + { + var expiration = new DateTime(2024, 12, 6); + var symbol = Symbol.CreateOption("SPY", Market.USA, OptionStyle.American, right, 450m, expiration); + var indicator = new Rho(symbol, 0.053m, 0.0153m, + optionModel: OptionPricingModelType.BinomialCoxRossRubinstein, ivModel: OptionPricingModelType.BlackScholes); + + var currentTime = expiration.AddHours(hoursAfterExpiryDate); + + var optionDataPoint = new IndicatorDataPoint(symbol, currentTime, price); + var spotDataPoint = new IndicatorDataPoint(symbol.Underlying, currentTime, spotPrice); + + Assert.IsFalse(indicator.Update(optionDataPoint)); + Assert.IsTrue(indicator.Update(spotDataPoint)); + + Assert.AreNotEqual(0, indicator.Current.Value); + } } } diff --git a/Tests/Indicators/ThetaTests.cs b/Tests/Indicators/ThetaTests.cs index a1619aeddc83..675d6de6fbdb 100644 --- a/Tests/Indicators/ThetaTests.cs +++ b/Tests/Indicators/ThetaTests.cs @@ -17,6 +17,7 @@ using QuantConnect.Algorithm; using QuantConnect.Data; using QuantConnect.Indicators; +using System; using System.IO; using System.Linq; @@ -127,5 +128,31 @@ public void ComparesAgainstExternalData2(decimal price, decimal spotPrice, Optio Assert.AreEqual(refTheta, (double)indicator.Current.Value, 0.00052d); } + + [TestCase(0.5, 470.0, OptionRight.Put, 0)] + [TestCase(0.5, 470.0, OptionRight.Put, 5)] + [TestCase(0.5, 470.0, OptionRight.Put, 10)] + [TestCase(0.5, 470.0, OptionRight.Put, 15)] + [TestCase(15.0, 450.0, OptionRight.Call, 0)] + [TestCase(15.0, 450.0, OptionRight.Call, 5)] + [TestCase(15.0, 450.0, OptionRight.Call, 10)] + [TestCase(0.5, 450.0, OptionRight.Call, 15)] + public void CanComputeOnExpirationDate(decimal price, decimal spotPrice, OptionRight right, int hoursAfterExpiryDate) + { + var expiration = new DateTime(2024, 12, 6); + var symbol = Symbol.CreateOption("SPY", Market.USA, OptionStyle.American, right, 450m, expiration); + var indicator = new Theta(symbol, 0.0403m, 0.0m, + optionModel: OptionPricingModelType.BinomialCoxRossRubinstein, ivModel: OptionPricingModelType.BlackScholes); + + var currentTime = expiration.AddHours(hoursAfterExpiryDate); + + var optionDataPoint = new IndicatorDataPoint(symbol, currentTime, price); + var spotDataPoint = new IndicatorDataPoint(symbol.Underlying, currentTime, spotPrice); + + Assert.IsFalse(indicator.Update(optionDataPoint)); + Assert.IsTrue(indicator.Update(spotDataPoint)); + + Assert.AreNotEqual(0, indicator.Current.Value); + } } } diff --git a/Tests/Indicators/VegaTests.cs b/Tests/Indicators/VegaTests.cs index 9414f35a23da..7c3af6948155 100644 --- a/Tests/Indicators/VegaTests.cs +++ b/Tests/Indicators/VegaTests.cs @@ -17,6 +17,7 @@ using QuantConnect.Algorithm; using QuantConnect.Data; using QuantConnect.Indicators; +using System; using System.IO; using System.Linq; @@ -127,5 +128,31 @@ public void ComparesAgainstExternalData2(decimal price, decimal spotPrice, Optio Assert.AreEqual(refVega, (double)indicator.Current.Value, 0.0021d); } + + [TestCase(0.5, 470.0, OptionRight.Put, 0)] + [TestCase(0.5, 470.0, OptionRight.Put, 5)] + [TestCase(0.5, 470.0, OptionRight.Put, 10)] + [TestCase(0.5, 470.0, OptionRight.Put, 15)] + [TestCase(15.0, 450.0, OptionRight.Call, 0)] + [TestCase(15.0, 450.0, OptionRight.Call, 5)] + [TestCase(15.0, 450.0, OptionRight.Call, 10)] + [TestCase(0.5, 450.0, OptionRight.Call, 15)] + public void CanComputeOnExpirationDate(decimal price, decimal spotPrice, OptionRight right, int hoursAfterExpiryDate) + { + var expiration = new DateTime(2024, 12, 6); + var symbol = Symbol.CreateOption("SPY", Market.USA, OptionStyle.American, right, 450m, expiration); + var indicator = new Vega(symbol, 0.053m, 0.0153m, + optionModel: OptionPricingModelType.BinomialCoxRossRubinstein, ivModel: OptionPricingModelType.BlackScholes); + + var currentTime = expiration.AddHours(hoursAfterExpiryDate); + + var optionDataPoint = new IndicatorDataPoint(symbol, currentTime, price); + var spotDataPoint = new IndicatorDataPoint(symbol.Underlying, currentTime, spotPrice); + + Assert.IsFalse(indicator.Update(optionDataPoint)); + Assert.IsTrue(indicator.Update(spotDataPoint)); + + Assert.AreNotEqual(0, indicator.Current.Value); + } } } From c878d1eefaff27026cf836a9231828d5f837297f Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 12 Dec 2024 18:38:25 -0400 Subject: [PATCH 2/7] Update tolerance in greek indicators tests --- Common/Securities/Option/OptionSymbol.cs | 2 ++ Tests/Indicators/DeltaTests.cs | 2 +- Tests/Indicators/ImpliedVolatilityTests.cs | 10 +++++----- Tests/Indicators/RhoTests.cs | 2 +- Tests/Indicators/ThetaTests.cs | 2 +- Tests/Indicators/VegaTests.cs | 2 +- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Common/Securities/Option/OptionSymbol.cs b/Common/Securities/Option/OptionSymbol.cs index 77653316e59b..1e3d789ac94f 100644 --- a/Common/Securities/Option/OptionSymbol.cs +++ b/Common/Securities/Option/OptionSymbol.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using QuantConnect.Securities.Future; using QuantConnect.Securities.IndexOption; @@ -152,6 +153,7 @@ public static bool IsOptionContractExpired(Symbol symbol, DateTime currentTimeUt return false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryGetExpirationDateTime(Symbol symbol, out DateTime expiryTime, out SecurityExchangeHours exchangeHours) { if (!symbol.SecurityType.IsOption()) diff --git a/Tests/Indicators/DeltaTests.cs b/Tests/Indicators/DeltaTests.cs index f5e3025ce4f8..5a1bbb84e6fb 100644 --- a/Tests/Indicators/DeltaTests.cs +++ b/Tests/Indicators/DeltaTests.cs @@ -125,7 +125,7 @@ public void ComparesAgainstExternalData2(decimal price, decimal spotPrice, Optio indicator.Update(optionDataPoint); indicator.Update(spotDataPoint); - Assert.AreEqual(refDelta, (double)indicator.Current.Value, 0.0005d); + Assert.AreEqual(refDelta, (double)indicator.Current.Value, 0.0017d); } [TestCase(0.5, 470.0, OptionRight.Put, 0)] diff --git a/Tests/Indicators/ImpliedVolatilityTests.cs b/Tests/Indicators/ImpliedVolatilityTests.cs index f24a25757782..0d1ba48732ad 100644 --- a/Tests/Indicators/ImpliedVolatilityTests.cs +++ b/Tests/Indicators/ImpliedVolatilityTests.cs @@ -106,7 +106,7 @@ public void SetSmoothingFunction(decimal price, decimal mirrorPrice, decimal spo indicator.Update(mirrorOptionDataPoint); indicator.Update(spotDataPoint); - Assert.AreEqual(refIV1, (double)indicator.Current.Value, 0.001d); + Assert.AreEqual(refIV1, (double)indicator.Current.Value, 0.0025d); indicator.SetSmoothingFunction((iv, mirrorIv) => iv); @@ -117,7 +117,7 @@ public void SetSmoothingFunction(decimal price, decimal mirrorPrice, decimal spo indicator.Update(mirrorOptionDataPoint); indicator.Update(spotDataPoint); - Assert.AreEqual(refIV2, (double)indicator.Current.Value, 0.001d); + Assert.AreEqual(refIV2, (double)indicator.Current.Value, 0.0035d); } [TestCase(23.753, 27.651, 450.0, OptionRight.Call, 60, 0.309, 0.309)] @@ -143,7 +143,7 @@ def TestSmoothingFunction(iv: float, mirror_iv: float) -> float: indicator.Update(mirrorOptionDataPoint); indicator.Update(spotDataPoint); - Assert.AreEqual(refIV1, (double)indicator.Current.Value, 0.001d); + Assert.AreEqual(refIV1, (double)indicator.Current.Value, 0.0025d); indicator.SetSmoothingFunction(pythonSmoothingFunction); @@ -154,7 +154,7 @@ def TestSmoothingFunction(iv: float, mirror_iv: float) -> float: indicator.Update(mirrorOptionDataPoint); indicator.Update(spotDataPoint); - Assert.AreEqual(refIV2, (double)indicator.Current.Value, 0.001d); + Assert.AreEqual(refIV2, (double)indicator.Current.Value, 0.0035d); } // Reference values from QuantLib @@ -180,7 +180,7 @@ public void ComparesAgainstExternalData2(decimal price, decimal spotPrice, Optio indicator.Update(optionDataPoint); indicator.Update(spotDataPoint); - Assert.AreEqual(refIV, (double)indicator.Current.Value, 0.001d); + Assert.AreEqual(refIV, (double)indicator.Current.Value, 0.0036d); } [TestCase(0.5, 470.0, OptionRight.Put, 0)] diff --git a/Tests/Indicators/RhoTests.cs b/Tests/Indicators/RhoTests.cs index 5436439093ce..a4ef179b549a 100644 --- a/Tests/Indicators/RhoTests.cs +++ b/Tests/Indicators/RhoTests.cs @@ -82,7 +82,7 @@ public void ComparesAgainstExternalData2(decimal price, decimal spotPrice, Optio indicator.Update(optionDataPoint); indicator.Update(spotDataPoint); - Assert.AreEqual(refRho, (double)indicator.Current.Value, 0.0011d); + Assert.AreEqual(refRho, (double)indicator.Current.Value, 0.017d); } [TestCase(0.5, 470.0, OptionRight.Put, 0)] diff --git a/Tests/Indicators/ThetaTests.cs b/Tests/Indicators/ThetaTests.cs index 675d6de6fbdb..049e5c2d1add 100644 --- a/Tests/Indicators/ThetaTests.cs +++ b/Tests/Indicators/ThetaTests.cs @@ -126,7 +126,7 @@ public void ComparesAgainstExternalData2(decimal price, decimal spotPrice, Optio indicator.Update(optionDataPoint); indicator.Update(spotDataPoint); - Assert.AreEqual(refTheta, (double)indicator.Current.Value, 0.00052d); + Assert.AreEqual(refTheta, (double)indicator.Current.Value, 0.0042d); } [TestCase(0.5, 470.0, OptionRight.Put, 0)] diff --git a/Tests/Indicators/VegaTests.cs b/Tests/Indicators/VegaTests.cs index 7c3af6948155..0d77acbc9a6d 100644 --- a/Tests/Indicators/VegaTests.cs +++ b/Tests/Indicators/VegaTests.cs @@ -126,7 +126,7 @@ public void ComparesAgainstExternalData2(decimal price, decimal spotPrice, Optio indicator.Update(optionDataPoint); indicator.Update(spotDataPoint); - Assert.AreEqual(refVega, (double)indicator.Current.Value, 0.0021d); + Assert.AreEqual(refVega, (double)indicator.Current.Value, 0.0072d); } [TestCase(0.5, 470.0, OptionRight.Put, 0)] From 45fb6134ce0804155f4a289f097a4dadcfa223ef Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 13 Dec 2024 11:12:10 -0400 Subject: [PATCH 3/7] Minor fix --- .../FutureOptionIndicatorsRegressionAlgorithm.cs | 2 +- .../IndexOptionIndicatorsRegressionAlgorithm.cs | 2 +- Algorithm.CSharp/OptionIndicatorsRegressionAlgorithm.cs | 2 +- Common/Securities/Option/OptionSymbol.cs | 7 +++---- Tests/Algorithm/AlgorithmIndicatorsTests.cs | 4 ++-- Tests/Common/Securities/Options/OptionSymbolTests.cs | 2 +- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Algorithm.CSharp/FutureOptionIndicatorsRegressionAlgorithm.cs b/Algorithm.CSharp/FutureOptionIndicatorsRegressionAlgorithm.cs index 96a22e915009..8a4e9674f079 100644 --- a/Algorithm.CSharp/FutureOptionIndicatorsRegressionAlgorithm.cs +++ b/Algorithm.CSharp/FutureOptionIndicatorsRegressionAlgorithm.cs @@ -22,7 +22,7 @@ namespace QuantConnect.Algorithm.CSharp { public class FutureOptionIndicatorsRegressionAlgorithm : OptionIndicatorsRegressionAlgorithm { - protected override string ExpectedGreeks { get; set; } = "Implied Volatility: 0.14008,Delta: 0.63466,Gamma: 0.00209,Vega: 5.61442,Theta: -0.48254,Rho: 0.03098"; + protected override string ExpectedGreeks { get; set; } = "Implied Volatility: 0.13941,Delta: 0.63509,Gamma: 0.00209,Vega: 5.64129,Theta: -0.47731,Rho: 0.03145"; public override void Initialize() { diff --git a/Algorithm.CSharp/IndexOptionIndicatorsRegressionAlgorithm.cs b/Algorithm.CSharp/IndexOptionIndicatorsRegressionAlgorithm.cs index 2d52e6a448eb..eaa6bd1f91f7 100644 --- a/Algorithm.CSharp/IndexOptionIndicatorsRegressionAlgorithm.cs +++ b/Algorithm.CSharp/IndexOptionIndicatorsRegressionAlgorithm.cs @@ -20,7 +20,7 @@ namespace QuantConnect.Algorithm.CSharp { public class IndexOptionIndicatorsRegressionAlgorithm : OptionIndicatorsRegressionAlgorithm { - protected override string ExpectedGreeks { get; set; } = "Implied Volatility: 0.17702,Delta: 0.19195,Gamma: 0.00247,Vega: 1.69043,Theta: -1.41571,Rho: 0.01686"; + protected override string ExpectedGreeks { get; set; } = "Implied Volatility: 0.17406,Delta: 0.19196,Gamma: 0.00247,Vega: 1.72195,Theta: -1.3689,Rho: 0.01744"; public override void Initialize() { diff --git a/Algorithm.CSharp/OptionIndicatorsRegressionAlgorithm.cs b/Algorithm.CSharp/OptionIndicatorsRegressionAlgorithm.cs index 6b5e0f68bb23..09aae25c7d8c 100644 --- a/Algorithm.CSharp/OptionIndicatorsRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionIndicatorsRegressionAlgorithm.cs @@ -32,7 +32,7 @@ public class OptionIndicatorsRegressionAlgorithm : QCAlgorithm, IRegressionAlgor private Theta _theta; private Rho _rho; - protected virtual string ExpectedGreeks { get; set; } = "Implied Volatility: 0.45252,Delta: -0.0092,Gamma: 0.00036,Vega: 0.03562,Theta: -0.0387,Rho: 0.00045"; + protected virtual string ExpectedGreeks { get; set; } = "Implied Volatility: 0.44529,Delta: -0.00921,Gamma: 0.00036,Vega: 0.03636,Theta: -0.03747,Rho: 0.00047"; public override void Initialize() { diff --git a/Common/Securities/Option/OptionSymbol.cs b/Common/Securities/Option/OptionSymbol.cs index 1e3d789ac94f..2d94b92cc382 100644 --- a/Common/Securities/Option/OptionSymbol.cs +++ b/Common/Securities/Option/OptionSymbol.cs @@ -189,11 +189,10 @@ private static bool TryGetExpirationDateTime(Symbol symbol, out DateTime expiryT } expiryTime = symbol.ID.Date.AddDays(1).Date; } - - // Standard index options are AM-settled, which means they settle the morning after the last trading day - if (symbol.SecurityType == SecurityType.IndexOption && IsStandard(symbol)) + // Standard index options are AM-settled, which means they settle on market open of the last trading date + else if (symbol.SecurityType == SecurityType.IndexOption && IsStandard(symbol)) { - expiryTime = exchangeHours.GetNextMarketOpen(expiryTime, false); + expiryTime = exchangeHours.GetNextMarketOpen(expiryTime.Date, false); } return true; diff --git a/Tests/Algorithm/AlgorithmIndicatorsTests.cs b/Tests/Algorithm/AlgorithmIndicatorsTests.cs index 1175b73154cc..ce7a6f3427de 100644 --- a/Tests/Algorithm/AlgorithmIndicatorsTests.cs +++ b/Tests/Algorithm/AlgorithmIndicatorsTests.cs @@ -398,8 +398,8 @@ public void IndicatorUpdatedWithSymbol(string testCase) } Assert.IsTrue(indicator.IsReady); - Assert.AreEqual(0.9942984m, indicator.Current.Value); - Assert.AreEqual(0.3516544m, indicator.ImpliedVolatility.Current.Value); + Assert.AreEqual(0.9942989m, indicator.Current.Value); + Assert.AreEqual(0.3514844m, indicator.ImpliedVolatility.Current.Value); Assert.AreEqual(390, indicatorValues.Count); var lastData = indicatorValues.Current.Last(); diff --git a/Tests/Common/Securities/Options/OptionSymbolTests.cs b/Tests/Common/Securities/Options/OptionSymbolTests.cs index 03f5e1a98481..0bb9b47c2074 100644 --- a/Tests/Common/Securities/Options/OptionSymbolTests.cs +++ b/Tests/Common/Securities/Options/OptionSymbolTests.cs @@ -86,7 +86,7 @@ private static IEnumerable ExpirationDateTimeTestCases() var amSettledIndexOption = Symbol.CreateOption(Symbols.SPX, "SPX", Market.USA, OptionStyle.European, OptionRight.Call, 200m, new DateTime(2016, 02, 18)); - yield return new TestCaseData(amSettledIndexOption, new DateTime(2016, 02, 19, 8, 30, 0)); + yield return new TestCaseData(amSettledIndexOption, new DateTime(2016, 02, 18, 8, 30, 0)); } [TestCaseSource(nameof(ExpirationDateTimeTestCases))] From c648de8051b9ffb5eef82f121954754f6c864375 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 13 Dec 2024 15:49:50 -0400 Subject: [PATCH 4/7] Modify helper method to calculate settlement time instead of expiration time --- Common/Securities/Option/OptionSymbol.cs | 19 ++++++++++--------- Indicators/ImpliedVolatility.cs | 2 +- Indicators/OptionGreekIndicatorBase.cs | 2 +- .../Securities/Options/OptionSymbolTests.cs | 6 +++--- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Common/Securities/Option/OptionSymbol.cs b/Common/Securities/Option/OptionSymbol.cs index 2d94b92cc382..f5c98abb5c74 100644 --- a/Common/Securities/Option/OptionSymbol.cs +++ b/Common/Securities/Option/OptionSymbol.cs @@ -122,17 +122,23 @@ public static DateTime GetLastDayOfTrading(Symbol symbol) } /// - /// Returns the actual expiration date time, adjusted to market close of the expiration day. + /// Returns the settlement date time of the option contract. /// /// The option contract symbol - /// The expiration date time, adjusted to market close of the expiration day - public static DateTime GetExpirationDateTime(Symbol symbol) + /// The settlement date time + public static DateTime GetSettlementDateTime(Symbol symbol) { - if (!TryGetExpirationDateTime(symbol, out var expiryTime, out _)) + if (!TryGetExpirationDateTime(symbol, out var expiryTime, out var exchangeHours)) { throw new ArgumentException("The symbol must be an option type"); } + // Standard index options are AM-settled, which means they settle on market open of the expiration date + if (expiryTime.Date == symbol.ID.Date.Date && symbol.SecurityType == SecurityType.IndexOption && IsStandard(symbol)) + { + expiryTime = exchangeHours.GetNextMarketOpen(expiryTime.Date, false); + } + return expiryTime; } @@ -189,11 +195,6 @@ private static bool TryGetExpirationDateTime(Symbol symbol, out DateTime expiryT } expiryTime = symbol.ID.Date.AddDays(1).Date; } - // Standard index options are AM-settled, which means they settle on market open of the last trading date - else if (symbol.SecurityType == SecurityType.IndexOption && IsStandard(symbol)) - { - expiryTime = exchangeHours.GetNextMarketOpen(expiryTime.Date, false); - } return true; } diff --git a/Indicators/ImpliedVolatility.cs b/Indicators/ImpliedVolatility.cs index c3eb788cba18..14c89d1deb32 100644 --- a/Indicators/ImpliedVolatility.cs +++ b/Indicators/ImpliedVolatility.cs @@ -266,7 +266,7 @@ protected override decimal Calculate(IndicatorDataPoint input) DividendYield.Update(time, _dividendYieldModel.GetDividendYield(time, UnderlyingPrice.Current.Value)); var timeTillExpiry = Convert.ToDecimal( - OptionGreekIndicatorsHelper.TimeTillExpiry(Securities.Option.OptionSymbol.GetExpirationDateTime(OptionSymbol), time)); + OptionGreekIndicatorsHelper.TimeTillExpiry(Securities.Option.OptionSymbol.GetSettlementDateTime(OptionSymbol), time)); _impliedVolatility = CalculateIV(timeTillExpiry); } diff --git a/Indicators/OptionGreekIndicatorBase.cs b/Indicators/OptionGreekIndicatorBase.cs index edbc04c08954..c93e47a33f50 100644 --- a/Indicators/OptionGreekIndicatorBase.cs +++ b/Indicators/OptionGreekIndicatorBase.cs @@ -184,7 +184,7 @@ protected override decimal Calculate(IndicatorDataPoint input) DividendYield.Update(time, _dividendYieldModel.GetDividendYield(time, UnderlyingPrice.Current.Value)); var timeTillExpiry = Convert.ToDecimal( - OptionGreekIndicatorsHelper.TimeTillExpiry(Securities.Option.OptionSymbol.GetExpirationDateTime(OptionSymbol), time)); + OptionGreekIndicatorsHelper.TimeTillExpiry(Securities.Option.OptionSymbol.GetSettlementDateTime(OptionSymbol), time)); try { _greekValue = timeTillExpiry < 0 ? 0 : CalculateGreek(timeTillExpiry); diff --git a/Tests/Common/Securities/Options/OptionSymbolTests.cs b/Tests/Common/Securities/Options/OptionSymbolTests.cs index 0bb9b47c2074..f87317d42b2d 100644 --- a/Tests/Common/Securities/Options/OptionSymbolTests.cs +++ b/Tests/Common/Securities/Options/OptionSymbolTests.cs @@ -90,10 +90,10 @@ private static IEnumerable ExpirationDateTimeTestCases() } [TestCaseSource(nameof(ExpirationDateTimeTestCases))] - public void CalculatesExpirationDateTime(Symbol symbol, DateTime expectedExpirationDateTime) + public void CalculatesSettlementDateTime(Symbol symbol, DateTime expectedSettlementDateTime) { - var expirationDateTime = OptionSymbol.GetExpirationDateTime(symbol); - Assert.AreEqual(expectedExpirationDateTime, expirationDateTime); + var settlementDateTime = OptionSymbol.GetSettlementDateTime(symbol); + Assert.AreEqual(expectedSettlementDateTime, settlementDateTime); } } } From e2937a6089e4f6623c266460b90ac06392d1f380 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 13 Dec 2024 17:09:19 -0400 Subject: [PATCH 5/7] Cache option expiration date time --- Common/Securities/Option/OptionSymbol.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Common/Securities/Option/OptionSymbol.cs b/Common/Securities/Option/OptionSymbol.cs index f5c98abb5c74..76483d0d19c1 100644 --- a/Common/Securities/Option/OptionSymbol.cs +++ b/Common/Securities/Option/OptionSymbol.cs @@ -28,6 +28,8 @@ public static class OptionSymbol { private static readonly Dictionary _optionExpirationErrorLog = new(); + private static readonly Dictionary _expirationDateTimes = new(); + /// /// Returns true if the option is a standard contract that expires 3rd Friday of the month /// @@ -130,7 +132,7 @@ public static DateTime GetSettlementDateTime(Symbol symbol) { if (!TryGetExpirationDateTime(symbol, out var expiryTime, out var exchangeHours)) { - throw new ArgumentException("The symbol must be an option type"); + throw new ArgumentException($"The symbol {symbol} is not an option type"); } // Standard index options are AM-settled, which means they settle on market open of the expiration date @@ -171,6 +173,11 @@ private static bool TryGetExpirationDateTime(Symbol symbol, out DateTime expiryT exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType); + if (_expirationDateTimes.TryGetValue(symbol, out expiryTime)) + { + return true; + } + // Ideally we can calculate expiry on the date of the symbol ID, but if that exchange is not open on that day we // will consider expired on the last trading day close before this; Example in AddOptionContractExpiresRegressionAlgorithm var lastTradingDay = exchangeHours.IsDateOpen(symbol.ID.Date) @@ -196,6 +203,8 @@ private static bool TryGetExpirationDateTime(Symbol symbol, out DateTime expiryT expiryTime = symbol.ID.Date.AddDays(1).Date; } + _expirationDateTimes[symbol] = expiryTime; + return true; } } From da392b18153725bb8d14776690b766b4232f291c Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 13 Dec 2024 17:24:42 -0400 Subject: [PATCH 6/7] Minor changes --- Indicators/ImpliedVolatility.cs | 3 +-- Indicators/OptionGreekIndicatorBase.cs | 3 +-- Indicators/OptionIndicatorBase.cs | 14 +++++++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Indicators/ImpliedVolatility.cs b/Indicators/ImpliedVolatility.cs index 14c89d1deb32..db35d023ddfe 100644 --- a/Indicators/ImpliedVolatility.cs +++ b/Indicators/ImpliedVolatility.cs @@ -265,8 +265,7 @@ protected override decimal Calculate(IndicatorDataPoint input) RiskFreeRate.Update(time, _riskFreeInterestRateModel.GetInterestRate(time)); DividendYield.Update(time, _dividendYieldModel.GetDividendYield(time, UnderlyingPrice.Current.Value)); - var timeTillExpiry = Convert.ToDecimal( - OptionGreekIndicatorsHelper.TimeTillExpiry(Securities.Option.OptionSymbol.GetSettlementDateTime(OptionSymbol), time)); + var timeTillExpiry = Convert.ToDecimal(OptionGreekIndicatorsHelper.TimeTillExpiry(Expiry, time)); _impliedVolatility = CalculateIV(timeTillExpiry); } diff --git a/Indicators/OptionGreekIndicatorBase.cs b/Indicators/OptionGreekIndicatorBase.cs index c93e47a33f50..2d40bd84d7b2 100644 --- a/Indicators/OptionGreekIndicatorBase.cs +++ b/Indicators/OptionGreekIndicatorBase.cs @@ -183,8 +183,7 @@ protected override decimal Calculate(IndicatorDataPoint input) RiskFreeRate.Update(time, _riskFreeInterestRateModel.GetInterestRate(time)); DividendYield.Update(time, _dividendYieldModel.GetDividendYield(time, UnderlyingPrice.Current.Value)); - var timeTillExpiry = Convert.ToDecimal( - OptionGreekIndicatorsHelper.TimeTillExpiry(Securities.Option.OptionSymbol.GetSettlementDateTime(OptionSymbol), time)); + var timeTillExpiry = Convert.ToDecimal(OptionGreekIndicatorsHelper.TimeTillExpiry(Expiry, time)); try { _greekValue = timeTillExpiry < 0 ? 0 : CalculateGreek(timeTillExpiry); diff --git a/Indicators/OptionIndicatorBase.cs b/Indicators/OptionIndicatorBase.cs index b8bef0a9ccdc..7b7ee0d8fa66 100644 --- a/Indicators/OptionIndicatorBase.cs +++ b/Indicators/OptionIndicatorBase.cs @@ -23,6 +23,8 @@ namespace QuantConnect.Indicators /// public abstract class OptionIndicatorBase : IndicatorBase, IIndicatorWarmUpPeriodProvider { + private DateTime _expiry; + /// /// Option's symbol object /// @@ -56,7 +58,17 @@ public abstract class OptionIndicatorBase : IndicatorBase, I /// /// Gets the expiration time of the option /// - public DateTime Expiry => OptionSymbol.ID.Date; + public DateTime Expiry + { + get + { + if (_expiry == default) + { + _expiry = Securities.Option.OptionSymbol.GetSettlementDateTime(OptionSymbol); + } + return _expiry; + } + } /// /// Gets the option right (call/put) of the option From 7813ce7524b5187c987439e192b357351a0fdb6d Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 13 Dec 2024 17:27:37 -0400 Subject: [PATCH 7/7] Minor changes --- Common/Securities/Option/OptionSymbol.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Common/Securities/Option/OptionSymbol.cs b/Common/Securities/Option/OptionSymbol.cs index 76483d0d19c1..9d2ac51404de 100644 --- a/Common/Securities/Option/OptionSymbol.cs +++ b/Common/Securities/Option/OptionSymbol.cs @@ -28,8 +28,6 @@ public static class OptionSymbol { private static readonly Dictionary _optionExpirationErrorLog = new(); - private static readonly Dictionary _expirationDateTimes = new(); - /// /// Returns true if the option is a standard contract that expires 3rd Friday of the month /// @@ -173,11 +171,6 @@ private static bool TryGetExpirationDateTime(Symbol symbol, out DateTime expiryT exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType); - if (_expirationDateTimes.TryGetValue(symbol, out expiryTime)) - { - return true; - } - // Ideally we can calculate expiry on the date of the symbol ID, but if that exchange is not open on that day we // will consider expired on the last trading day close before this; Example in AddOptionContractExpiresRegressionAlgorithm var lastTradingDay = exchangeHours.IsDateOpen(symbol.ID.Date) @@ -203,8 +196,6 @@ private static bool TryGetExpirationDateTime(Symbol symbol, out DateTime expiryT expiryTime = symbol.ID.Date.AddDays(1).Date; } - _expirationDateTimes[symbol] = expiryTime; - return true; } }