Skip to content

Commit

Permalink
Greeks calculation on expiration date (#8465)
Browse files Browse the repository at this point in the history
* 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

* Update tolerance in greek indicators tests

* Minor fix

* Modify helper method to calculate settlement time instead of expiration time

* Cache option expiration date time

* Minor changes

* Minor changes
  • Loading branch information
jhonabreul authored Dec 13, 2024
1 parent fcb4ce6 commit a8592e0
Show file tree
Hide file tree
Showing 14 changed files with 262 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
2 changes: 1 addition & 1 deletion Algorithm.CSharp/OptionIndicatorsRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
49 changes: 41 additions & 8 deletions Common/Securities/Option/OptionSymbol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.IndexOption;

Expand Down Expand Up @@ -120,31 +121,63 @@ public static DateTime GetLastDayOfTrading(Symbol symbol)
return symbolDateTime.AddDays(daysBefore).Date;
}

/// <summary>
/// Returns the settlement date time of the option contract.
/// </summary>
/// <param name="symbol">The option contract symbol</param>
/// <returns>The settlement date time</returns>
public static DateTime GetSettlementDateTime(Symbol symbol)
{
if (!TryGetExpirationDateTime(symbol, out var expiryTime, out var exchangeHours))
{
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
if (expiryTime.Date == symbol.ID.Date.Date && symbol.SecurityType == SecurityType.IndexOption && IsStandard(symbol))
{
expiryTime = exchangeHours.GetNextMarketOpen(expiryTime.Date, false);
}

return expiryTime;
}

/// <summary>
/// Returns true if the option contract is expired at the specified time
/// </summary>
/// <param name="symbol">The option contract symbol</param>
/// <param name="currentTimeUtc">The current time (UTC)</param>
/// <returns>True if the option contract is expired at the specified time, false otherwise</returns>
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;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
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.
Expand All @@ -163,7 +196,7 @@ public static bool IsOptionContractExpired(Symbol symbol, DateTime currentTimeUt
expiryTime = symbol.ID.Date.AddDays(1).Date;
}

return currentTime >= expiryTime;
return true;
}
}
}
1 change: 0 additions & 1 deletion Indicators/OptionGreekIndicatorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
using System;
using Python.Runtime;
using QuantConnect.Data;
using QuantConnect.Logging;
using QuantConnect.Python;

namespace QuantConnect.Indicators
Expand Down
14 changes: 13 additions & 1 deletion Indicators/OptionIndicatorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ namespace QuantConnect.Indicators
/// </summary>
public abstract class OptionIndicatorBase : IndicatorBase<IndicatorDataPoint>, IIndicatorWarmUpPeriodProvider
{
private DateTime _expiry;

/// <summary>
/// Option's symbol object
/// </summary>
Expand Down Expand Up @@ -56,7 +58,17 @@ public abstract class OptionIndicatorBase : IndicatorBase<IndicatorDataPoint>, I
/// <summary>
/// Gets the expiration time of the option
/// </summary>
public DateTime Expiry => OptionSymbol.ID.Date;
public DateTime Expiry
{
get
{
if (_expiry == default)
{
_expiry = Securities.Option.OptionSymbol.GetSettlementDateTime(OptionSymbol);
}
return _expiry;
}
}

/// <summary>
/// Gets the option right (call/put) of the option
Expand Down
4 changes: 2 additions & 2 deletions Tests/Algorithm/AlgorithmIndicatorsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
26 changes: 26 additions & 0 deletions Tests/Common/Securities/Options/OptionSymbolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

using System;
using System.Collections.Generic;
using NUnit.Framework;
using QuantConnect.Securities.Option;

Expand Down Expand Up @@ -69,5 +70,30 @@ public void IsOptionContractExpiredReturnsFalseIfTimeOfDayDiffer()

Assert.IsFalse(OptionSymbol.IsOptionContractExpired(symbol, new DateTime(2022, 03, 11)));
}

private static IEnumerable<TestCaseData> 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, 18, 8, 30, 0));
}

[TestCaseSource(nameof(ExpirationDateTimeTestCases))]
public void CalculatesSettlementDateTime(Symbol symbol, DateTime expectedSettlementDateTime)
{
var settlementDateTime = OptionSymbol.GetSettlementDateTime(symbol);
Assert.AreEqual(expectedSettlementDateTime, settlementDateTime);
}
}
}
29 changes: 28 additions & 1 deletion Tests/Indicators/DeltaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Indicators;
using System;
using System.IO;
using System.Linq;

Expand Down Expand Up @@ -124,7 +125,33 @@ 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)]
[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);
}
}
}
35 changes: 35 additions & 0 deletions Tests/Indicators/GammaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Indicators;
using System;
using System.IO;
using System.Linq;

Expand Down Expand Up @@ -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);
}
}
}
35 changes: 30 additions & 5 deletions Tests/Indicators/ImpliedVolatilityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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)]
Expand All @@ -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);

Expand All @@ -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
Expand All @@ -180,7 +180,32 @@ 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)]
[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]
Expand Down
Loading

0 comments on commit a8592e0

Please sign in to comment.