From 55c5f9848bd98b2a9ce4c24cd8769df4b26e4b0b Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 23 Jan 2026 13:37:00 -0400 Subject: [PATCH 1/2] Implement indicator-based option price model This model uses IV and Greeks indicators to implement Lean's own option pricing model --- ...cingModelIndexOptionRegressionAlgorithm.cs | 46 ++++ ...edOptionPricingModelRegressionAlgorithm.cs | 177 ++++++++++++ .../Option/OptionPriceModelResult.cs | 23 +- Indicators/ImpliedVolatility.cs | 22 +- Indicators/IndicatorBasedOptionPriceModel.cs | 260 ++++++++++++++++++ .../Securities/OptionPriceModelTests.cs | 2 +- .../IndicatorBasedOptionPriceModelTests.cs | 128 +++++++++ 7 files changed, 649 insertions(+), 9 deletions(-) create mode 100644 Algorithm.CSharp/IndicatorBasedOptionPricingModelIndexOptionRegressionAlgorithm.cs create mode 100644 Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs create mode 100644 Indicators/IndicatorBasedOptionPriceModel.cs create mode 100644 Tests/Indicators/IndicatorBasedOptionPriceModelTests.cs diff --git a/Algorithm.CSharp/IndicatorBasedOptionPricingModelIndexOptionRegressionAlgorithm.cs b/Algorithm.CSharp/IndicatorBasedOptionPricingModelIndexOptionRegressionAlgorithm.cs new file mode 100644 index 000000000000..e52f33dc8e97 --- /dev/null +++ b/Algorithm.CSharp/IndicatorBasedOptionPricingModelIndexOptionRegressionAlgorithm.cs @@ -0,0 +1,46 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +using QuantConnect.Indicators; +using QuantConnect.Securities.Option; +using System; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// This example demonstrates how to override the option pricing model with the + /// for a given index option security. + /// + public class IndicatorBasedOptionPricingModelIndexOptionRegressionAlgorithm : IndicatorBasedOptionPricingModelRegressionAlgorithm + { + protected override DateTime TestStartDate => new(2021, 1, 4); + + protected override DateTime TestEndDate => new(2021, 1, 4); + + protected override Option GetOption() + { + var index = AddIndex("SPX"); + var indexOption = AddIndexOption(index.Symbol); + indexOption.SetFilter(u => u.CallsOnly()); + return indexOption; + } + + /// + /// Data Points count of all timeslices of algorithm + /// + public override long DataPoints => 4806; + } +} diff --git a/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs b/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs new file mode 100644 index 000000000000..24c495dbcd32 --- /dev/null +++ b/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs @@ -0,0 +1,177 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +using System.Collections.Generic; +using QuantConnect.Data; +using QuantConnect.Interfaces; +using QuantConnect.Indicators; +using QuantConnect.Securities.Option; +using System; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// This example demonstrates how to override the option pricing model with the + /// for a given option security. + /// + public class IndicatorBasedOptionPricingModelRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private bool _checked; + + private Option _option; + + protected virtual DateTime TestStartDate => new(2015, 12, 24); + + protected virtual DateTime TestEndDate => new(2015, 12, 24); + + public override void Initialize() + { + SetStartDate(TestStartDate); + SetEndDate(TestEndDate); + SetCash(100000); + + _option = GetOption(); + _option.PriceModel = new IndicatorBasedOptionPriceModel(); + } + + protected virtual Option GetOption() + { + var equity = AddEquity("GOOG"); + var option = AddOption(equity.Symbol); + option.SetFilter(u => u.Strikes(-2, +2).Expiration(0, 180)); + return option; + } + + public override void OnData(Slice slice) + { + if (!_checked && slice.OptionChains.TryGetValue(_option.Symbol, out var chain)) + { + if (_option.PriceModel is not IndicatorBasedOptionPriceModel) + { + throw new RegressionTestException("Option pricing model was not set to IndicatorBasedOptionPriceModel"); + } + + foreach (var contract in chain) + { + var theoreticalPrice = contract.TheoreticalPrice; + var iv = contract.ImpliedVolatility; + var greeks = contract.Greeks; + + Log($"{contract.Symbol}:: Theoretical Price: {theoreticalPrice}, IV: {iv}, " + + $"Delta: {greeks.Delta}, Gamma: {greeks.Gamma}, Vega: {greeks.Vega}, " + + $"Theta: {greeks.Theta}, Rho: {greeks.Rho}, Lambda: {greeks.Lambda}"); + + // Sanity check values + if (theoreticalPrice <= 0) + { + throw new RegressionTestException($"Invalid theoretical price for {contract.Symbol}: {theoreticalPrice}"); + } + // We check for all greeks and IV together. e.g. IV could be zero if the model can't converge, say for instance if a contract is iliquid or deep ITM/OTM + if (greeks == null || + (iv == 0 && greeks.Delta == 0 && greeks.Gamma == 0 && greeks.Vega== 0 && greeks.Theta == 0 && greeks.Rho == 0)) + { + throw new RegressionTestException($"Invalid Greeks for {contract.Symbol}"); + } + + // Manually evaluate the price model, just in case + var result = _option.EvaluatePriceModel(slice, contract); + + if (result == null || + result.TheoreticalPrice != theoreticalPrice || + result.ImpliedVolatility != iv || + result.Greeks.Delta != greeks.Delta || + result.Greeks.Gamma != greeks.Gamma || + result.Greeks.Vega != greeks.Vega || + result.Greeks.Theta != greeks.Theta || + result.Greeks.Rho != greeks.Rho) + { + throw new RegressionTestException($"EvaluatePriceModel returned different results for {contract.Symbol}"); + } + + _checked |= true; + } + } + } + + public override void OnEndOfAlgorithm() + { + if (!_checked) + { + throw new RegressionTestException("Option chain was never received."); + } + } + + /// + /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm. + /// + public bool CanRunLocally { get; } = true; + + /// + /// This is used by the regression test system to indicate which languages this algorithm is written in. + /// + public List Languages { get; } = new() { Language.CSharp }; + + /// + /// Data Points count of all timeslices of algorithm + /// + public virtual long DataPoints => 37131; + + /// + /// Data Points count of the algorithm history + /// + public int AlgorithmHistoryDataPoints => 0; + + /// + /// Final status of the algorithm + /// + public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed; + + /// + /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm + /// + public Dictionary ExpectedStatistics => new Dictionary + { + {"Total Orders", "0"}, + {"Average Win", "0%"}, + {"Average Loss", "0%"}, + {"Compounding Annual Return", "0%"}, + {"Drawdown", "0%"}, + {"Expectancy", "0"}, + {"Start Equity", "100000"}, + {"End Equity", "100000"}, + {"Net Profit", "0%"}, + {"Sharpe Ratio", "0"}, + {"Sortino Ratio", "0"}, + {"Probabilistic Sharpe Ratio", "0%"}, + {"Loss Rate", "0%"}, + {"Win Rate", "0%"}, + {"Profit-Loss Ratio", "0"}, + {"Alpha", "0"}, + {"Beta", "0"}, + {"Annual Standard Deviation", "0"}, + {"Annual Variance", "0"}, + {"Information Ratio", "0"}, + {"Tracking Error", "0"}, + {"Treynor Ratio", "0"}, + {"Total Fees", "$0.00"}, + {"Estimated Strategy Capacity", "$0"}, + {"Lowest Capacity Asset", ""}, + {"Portfolio Turnover", "0%"}, + {"Drawdown Recovery", "0"}, + {"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"} + }; + } +} diff --git a/Common/Securities/Option/OptionPriceModelResult.cs b/Common/Securities/Option/OptionPriceModelResult.cs index 417d0f6efc74..e42e8098578a 100644 --- a/Common/Securities/Option/OptionPriceModelResult.cs +++ b/Common/Securities/Option/OptionPriceModelResult.cs @@ -28,6 +28,7 @@ public class OptionPriceModelResult /// public static OptionPriceModelResult None { get; } = new(0, NullGreeks.Instance); + private readonly Lazy _theoreticalPrice; private readonly Lazy _greeks; private readonly Lazy _impliedVolatility; @@ -36,7 +37,10 @@ public class OptionPriceModelResult /// public decimal TheoreticalPrice { - get; private set; + get + { + return _theoreticalPrice.Value; + } } /// @@ -67,10 +71,8 @@ public Greeks Greeks /// The theoretical price computed by the price model /// The sensitivities (greeks) computed by the price model public OptionPriceModelResult(decimal theoreticalPrice, Greeks greeks) + : this(() => theoreticalPrice, () => decimal.Zero, () => greeks) { - TheoreticalPrice = theoreticalPrice; - _impliedVolatility = new Lazy(() => 0m, isThreadSafe: false); - _greeks = new Lazy(() => greeks, isThreadSafe: false); } /// @@ -80,8 +82,19 @@ public OptionPriceModelResult(decimal theoreticalPrice, Greeks greeks) /// The calculated implied volatility /// The sensitivities (greeks) computed by the price model public OptionPriceModelResult(decimal theoreticalPrice, Func impliedVolatility, Func greeks) + : this(() => theoreticalPrice, impliedVolatility, greeks) + { + } + + /// + /// Initializes a new instance of the class with lazy calculations of implied volatility and greeks + /// + /// The theoretical price computed by the price model + /// The calculated implied volatility + /// The sensitivities (greeks) computed by the price model + public OptionPriceModelResult(Func theoreticalPrice, Func impliedVolatility, Func greeks) { - TheoreticalPrice = theoreticalPrice; + _theoreticalPrice = new Lazy(theoreticalPrice, isThreadSafe: false); _impliedVolatility = new Lazy(impliedVolatility, isThreadSafe: false); _greeks = new Lazy(greeks, isThreadSafe: false); } diff --git a/Indicators/ImpliedVolatility.cs b/Indicators/ImpliedVolatility.cs index 6b3b656f1d4f..18aacc103c7f 100644 --- a/Indicators/ImpliedVolatility.cs +++ b/Indicators/ImpliedVolatility.cs @@ -13,13 +13,13 @@ * limitations under the License. */ -using System; using MathNet.Numerics.RootFinding; using Python.Runtime; using QuantConnect.Data; using QuantConnect.Logging; using QuantConnect.Python; using QuantConnect.Util; +using System; namespace QuantConnect.Indicators { @@ -31,6 +31,11 @@ public class ImpliedVolatility : OptionIndicatorBase private decimal _impliedVolatility; private Func SmoothingFunction; + /// + /// Gets the theoretical option price + /// + public IndicatorBase TheoreticalPrice { get; } + /// /// Initializes a new instance of the ImpliedVolatility class /// @@ -63,6 +68,17 @@ public ImpliedVolatility(string name, Symbol option, IRiskFreeInterestRateModel return impliedVol; }; } + + TheoreticalPrice = new FunctionalIndicator($"{name}_TheoreticalPrice", + (iv) => + { + var theoreticalPrice = CalculateTheoreticalPrice((double)iv.Value, (double)UnderlyingPrice.Current.Value, (double)Strike, + OptionGreekIndicatorsHelper.TimeTillExpiry(Expiry, iv.EndTime), (double)RiskFreeRate.Current.Value, (double)DividendYield.Current.Value, + Right, optionModel); + return Convert.ToDecimal(theoreticalPrice); + }, + _ => IsReady) + .Of(this); } /// @@ -238,7 +254,7 @@ protected override decimal ComputeIndicator() } // Calculate the theoretical option price - private static double TheoreticalPrice(double volatility, double spotPrice, double strikePrice, double timeTillExpiry, double riskFreeRate, + private static double CalculateTheoreticalPrice(double volatility, double spotPrice, double strikePrice, double timeTillExpiry, double riskFreeRate, double dividendYield, OptionRight optionType, OptionPricingModelType? optionModel = null) { if (timeTillExpiry <= 0) @@ -302,7 +318,7 @@ protected virtual decimal CalculateIV(decimal timeTillExpiry) decimal? impliedVol = null; try { - Func f = (vol) => TheoreticalPrice(vol, underlyingPrice, strike, timeTillExpiry, riskFreeRate, dividendYield, right, optionModel) - optionPrice; + Func f = (vol) => CalculateTheoreticalPrice(vol, underlyingPrice, strike, timeTillExpiry, riskFreeRate, dividendYield, right, optionModel) - optionPrice; impliedVol = Convert.ToDecimal(Brent.FindRoot(f, lowerBound, upperBound, accuracy, 100)); } catch diff --git a/Indicators/IndicatorBasedOptionPriceModel.cs b/Indicators/IndicatorBasedOptionPriceModel.cs new file mode 100644 index 000000000000..6171e7f02982 --- /dev/null +++ b/Indicators/IndicatorBasedOptionPriceModel.cs @@ -0,0 +1,260 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using QuantConnect.Data; +using QuantConnect.Data.Market; +using QuantConnect.Logging; +using QuantConnect.Securities; +using QuantConnect.Securities.Option; +using System; +using System.Linq; + +namespace QuantConnect.Indicators +{ + /// + /// Provides an implementation of that uses QuantConnect indicators + /// to provide a theoretical price for the option contract. + /// + public class IndicatorBasedOptionPriceModel : IOptionPriceModel + { + /// + /// Creates a new containing the theoretical price based on + /// QuantConnect indicators. + /// + /// The option security object + /// + /// The current data slice. This can be used to access other information + /// available to the algorithm + /// + /// The option contract to evaluate + /// + /// An instance of containing the theoretical + /// price of the specified option contract. + /// + public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionContract contract) + { + // expired options have no price + if (contract.Time.Date > contract.Expiry.Date) + { + if (Log.DebuggingEnabled) + { + Log.Debug($"IndicatorBasedOptionPriceModel.Evaluate(). Expired {contract.Symbol}. Time > Expiry: {contract.Time.Date} > {contract.Expiry.Date}"); + } + return OptionPriceModelResult.None; + } + + var contractSymbol = contract.Symbol; + var underlyingData = slice.AllData + // We use trades for the underlying (see how Greeks indicators are registered to algorithms) + .Where(x => x.Symbol == contractSymbol.Underlying && (x is TradeBar || (x is Tick tick && tick.TickType == TickType.Trade))) + // Order by resolution + .OrderBy(x => x.EndTime - x.Time) + // Let's use the lowest resolution available, trying to match our pre calculated daily greeks (using daily bars if possible). + // If ticks, use the last tick in the slice + .LastOrDefault(); + + var period = TimeSpan.Zero; + BaseData optionData = null; + if (underlyingData != null) + { + period = underlyingData.EndTime - underlyingData.Time; + optionData = slice.AllData + .Where(x => x.Symbol == contractSymbol && + // Use the same resolution data + x.EndTime - x.Time == period && + // We use quotes for the options (see how Greeks indicators are registered to algorithms) + (x is QuoteBar || (x is Tick tick && tick.TickType == TickType.Quote))) + .LastOrDefault(); + } + + if (underlyingData == null || optionData == null) + { + if (Log.DebuggingEnabled) + { + Log.Debug($"IndicatorBasedOptionPriceModel.Evaluate(). Missing data for {contractSymbol} or {contractSymbol.Underlying}."); + } + return OptionPriceModelResult.None; + } + + var mirrorContractSymbol = Symbol.CreateOption(contractSymbol.Underlying, + contractSymbol.ID.Symbol, + contractSymbol.ID.Market, + contractSymbol.ID.OptionStyle, + contractSymbol.ID.OptionRight == OptionRight.Call ? OptionRight.Put : OptionRight.Call, + contractSymbol.ID.StrikePrice, + contractSymbol.ID.Date); + var mirrorOptionData = slice.AllData + .Where(x => x.Symbol == mirrorContractSymbol && + // Use the same resolution data + x.EndTime - x.Time == period && + (x is QuoteBar || (x is Tick tick && tick.TickType == TickType.Quote))) + .LastOrDefault(); + + if (mirrorOptionData == null) + { + if (Log.DebuggingEnabled) + { + Log.Debug($"IndicatorBasedOptionPriceModel.Evaluate(). Missing data for mirror option {mirrorContractSymbol}. Using contract symbol only."); + } + mirrorContractSymbol = null; + } + + var greeksIndicators = new Lazy(() => + { + var indicators = new GreeksIndicators(contractSymbol, mirrorContractSymbol); + + if (underlyingData != null) + { + indicators.Update(underlyingData); + } + if (optionData != null) + { + indicators.Update(optionData); + } + if (mirrorOptionData != null) + { + indicators.Update(mirrorOptionData); + } + + return indicators; + }, isThreadSafe: false); + + return new OptionPriceModelResult( + () => greeksIndicators.Value.ImpliedVolatility.TheoreticalPrice, + () => greeksIndicators.Value.ImpliedVolatility, + () => greeksIndicators.Value.Greeks); + } + } + + /// + /// Helper class that holds and updates the greeks indicators + /// + public class GreeksIndicators + { + private readonly static IRiskFreeInterestRateModel _interestRateProvider = new InterestRateProvider(); + + private readonly Symbol _optionSymbol; + private readonly Symbol _mirrorOptionSymbol; + + /// + /// Gets the implied volatility indicator + /// + public ImpliedVolatility ImpliedVolatility { get; } + + /// + /// Gets the delta indicator + /// + public Delta Delta { get; } + + /// + /// Gets the gamma indicator + /// + public Gamma Gamma { get; } + + /// + /// Gets the vega indicator + /// + public Vega Vega { get; } + + /// + /// Gets the theta indicator + /// + public Theta Theta { get; } + + /// + /// Gets the rho indicator + /// + public Rho Rho { get; } + + /// + /// Gets the interest rate used in the calculations + /// + public decimal InterestRate => Delta.RiskFreeRate; + + /// + /// Gets the dividend yield used in the calculations + /// + public decimal DividendYield => Delta.DividendYield; + + /// + /// Gets the current greeks values + /// + public Greeks Greeks => new GreeksHolder(Delta, Gamma, Vega, Theta, Rho); + + /// + /// Creates a new instance of the class + /// + public GreeksIndicators(Symbol optionSymbol, Symbol mirrorOptionSymbol, OptionPricingModelType? optionModel = null, + OptionPricingModelType? ivModel = null) + { + _optionSymbol = optionSymbol; + _mirrorOptionSymbol = mirrorOptionSymbol; + + IDividendYieldModel dividendYieldModel = optionSymbol.SecurityType != SecurityType.IndexOption + ? DividendYieldProvider.CreateForOption(_optionSymbol) + : new ConstantDividendYieldModel(0); + + ImpliedVolatility = new ImpliedVolatility(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, ivModel); + Delta = new Delta(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + Gamma = new Gamma(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + Vega = new Vega(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + Theta = new Theta(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + Rho = new Rho(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + + Delta.ImpliedVolatility = ImpliedVolatility; + Gamma.ImpliedVolatility = ImpliedVolatility; + Vega.ImpliedVolatility = ImpliedVolatility; + Theta.ImpliedVolatility = ImpliedVolatility; + Rho.ImpliedVolatility = ImpliedVolatility; + } + + /// + /// Feeds the specified data into the indicators + /// + public void Update(IBaseData data) + { + ImpliedVolatility.Update(data); + Delta.Update(data); + Gamma.Update(data); + Vega.Update(data); + Theta.Update(data); + Rho.Update(data); + } + + private class GreeksHolder : Greeks + { + public override decimal Delta { get; } + + public override decimal Gamma { get; } + + public override decimal Vega { get; } + + public override decimal Theta { get; } + + public override decimal Rho { get; } + + public override decimal Lambda { get; } + + public GreeksHolder(decimal delta, decimal gamma, decimal vega, decimal theta, decimal rho) + { + Delta = delta; + Gamma = gamma; + Vega = vega; + Theta = theta; + Rho = rho; + } + } + } +} diff --git a/Tests/Common/Securities/OptionPriceModelTests.cs b/Tests/Common/Securities/OptionPriceModelTests.cs index df2aa38c36cd..cd400c3f857c 100644 --- a/Tests/Common/Securities/OptionPriceModelTests.cs +++ b/Tests/Common/Securities/OptionPriceModelTests.cs @@ -968,7 +968,7 @@ public static Equity GetEquity(Symbol symbol, decimal underlyingPrice, decimal u return equity; } - public OptionContract GetOptionContract(Symbol symbol, Symbol underlying, DateTime evaluationDate) + public static OptionContract GetOptionContract(Symbol symbol, Symbol underlying, DateTime evaluationDate) { var option = CreateOption(symbol); return new OptionContract(option) { Time = evaluationDate }; diff --git a/Tests/Indicators/IndicatorBasedOptionPriceModelTests.cs b/Tests/Indicators/IndicatorBasedOptionPriceModelTests.cs new file mode 100644 index 000000000000..07911612ca7c --- /dev/null +++ b/Tests/Indicators/IndicatorBasedOptionPriceModelTests.cs @@ -0,0 +1,128 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using NUnit.Framework; +using QuantConnect.Data; +using QuantConnect.Data.Market; +using QuantConnect.Indicators; +using QuantConnect.Securities.Option; +using QuantConnect.Tests.Common; +using System; +using System.Collections.Generic; + +namespace QuantConnect.Tests.Indicators +{ + [TestFixture] + public class IndicatorBasedOptionPriceModelTests + { + [TestCase(true, 6.05391914652262, 0.3564563, 0.7560627, 0.0430897, 0.0662474, -4.3932945, 0.0000902)] + [TestCase(false, 5.05413609164657, 0.1428964, 0.9574846, 0.0311305, 0.0205564, -0.4502054, 0.0000057)] + public void WorksWithAndWithoutMirrorContract([Values] bool withMirrorContract, decimal expectedTheoreticalPrice, + decimal expectedIv, decimal expectedDelta, decimal expectedGamma, decimal expectedVega, + decimal expectedTheta, decimal expectedRho) + { + GetTestData(true, true, withMirrorContract, out var option, out var contract, out var slice); + + var model = new IndicatorBasedOptionPriceModel(); + + var result = model.Evaluate(option, slice, contract); + var theoreticalPrice = result.TheoreticalPrice; + var iv = result.ImpliedVolatility; + var greeks = result.Greeks; + + Assert.AreEqual(expectedTheoreticalPrice, theoreticalPrice); + Assert.AreEqual(expectedIv, iv); + Assert.AreEqual(expectedDelta, greeks.Delta); + Assert.AreEqual(expectedGamma, greeks.Gamma); + Assert.AreEqual(expectedVega, greeks.Vega); + Assert.AreEqual(expectedTheta, greeks.Theta); + Assert.AreEqual(expectedRho, greeks.Rho); + } + + [TestCase(false, false)] + [TestCase(true, false)] + [TestCase(false, true)] + public void WontCalculateIfMissindData(bool withUnderlyingData, bool withOptionData) + { + GetTestData(withUnderlyingData, withOptionData, true, out var option, out var contract, out var slice); + + var model = new IndicatorBasedOptionPriceModel(); + var result = model.Evaluate(option, slice, contract); + + Assert.AreEqual(OptionPriceModelResult.None, result); + } + + private static void GetTestData(bool withUnderlying, bool withOption, bool withMirrorOption, + out Option option, out OptionContract contract, out Slice slice) + { + var underlyingSymbol = Symbols.GOOG; + var date = new DateTime(2015, 11, 24); + var contractSymbol = Symbols.CreateOptionSymbol(underlyingSymbol.Value, OptionRight.Call, 745m, date); + + var tz = TimeZones.NewYork; + var underlyingPrice = 750m; + var underlyingVolume = 10000; + var contractPrice = 5m; + var underlying = OptionPriceModelTests.GetEquity(underlyingSymbol, underlyingPrice, underlyingVolume, tz); + option = OptionPriceModelTests.GetOption(contractSymbol, underlying, tz); + contract = OptionPriceModelTests.GetOptionContract(contractSymbol, underlyingSymbol, date); + + var time = date.Add(new TimeSpan(9, 31, 0)); + + var data = new List(); + + if (withUnderlying) + { + var underlyingData = new TradeBar(time, underlyingSymbol, underlyingPrice, underlyingPrice, underlyingPrice, underlyingPrice, underlyingVolume, TimeSpan.FromMinutes(1)); + data.Add(underlyingData); + underlying.SetMarketPrice(underlyingData); + } + + if (withOption) + { + var contractData = new QuoteBar(time, + contractSymbol, + new Bar(contractPrice, contractPrice, contractPrice, contractPrice), + 10, + new Bar(contractPrice + 0.1m, contractPrice + 0.1m, contractPrice + 0.1m, contractPrice + 0.1m), + 10, + TimeSpan.FromMinutes(1)); + data.Add(contractData); + option.SetMarketPrice(contractData); + } + + if (withMirrorOption) + { + var mirrorContractSymbol = Symbol.CreateOption(contractSymbol.Underlying, + contractSymbol.ID.Symbol, + contractSymbol.ID.Market, + contractSymbol.ID.OptionStyle, + contractSymbol.ID.OptionRight == OptionRight.Call ? OptionRight.Put : OptionRight.Call, + contractSymbol.ID.StrikePrice, + contractSymbol.ID.Date); + var mirrorContractPrice = 1m; + data.Add(new QuoteBar(time, + mirrorContractSymbol, + new Bar(mirrorContractPrice, mirrorContractPrice, mirrorContractPrice, mirrorContractPrice), + 10, + new Bar(mirrorContractPrice + 0.1m, mirrorContractPrice + 0.1m, mirrorContractPrice + 0.1m, mirrorContractPrice + 0.1m), + 10, + TimeSpan.FromMinutes(1))); + } + + slice = new Slice(time, data, time.ConvertToUtc(tz)); + } + } +} From da5936282f5935fa12c5802414ce7458fb54cfeb Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 23 Jan 2026 16:55:52 -0400 Subject: [PATCH 2/2] Minor fixes --- Indicators/GreeksIndicators.cs | 140 +++++++++++++++++++ Indicators/ImpliedVolatility.cs | 25 +++- Indicators/IndicatorBasedOptionPriceModel.cs | 120 ---------------- 3 files changed, 164 insertions(+), 121 deletions(-) create mode 100644 Indicators/GreeksIndicators.cs diff --git a/Indicators/GreeksIndicators.cs b/Indicators/GreeksIndicators.cs new file mode 100644 index 000000000000..c751f81c8d30 --- /dev/null +++ b/Indicators/GreeksIndicators.cs @@ -0,0 +1,140 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using QuantConnect.Data; +using QuantConnect.Data.Market; + +namespace QuantConnect.Indicators +{ + /// + /// Helper class that holds and updates the greeks indicators + /// + public class GreeksIndicators + { + private readonly static IRiskFreeInterestRateModel _interestRateProvider = new InterestRateProvider(); + + private readonly Symbol _optionSymbol; + private readonly Symbol _mirrorOptionSymbol; + + /// + /// Gets the implied volatility indicator + /// + public ImpliedVolatility ImpliedVolatility { get; } + + /// + /// Gets the delta indicator + /// + public Delta Delta { get; } + + /// + /// Gets the gamma indicator + /// + public Gamma Gamma { get; } + + /// + /// Gets the vega indicator + /// + public Vega Vega { get; } + + /// + /// Gets the theta indicator + /// + public Theta Theta { get; } + + /// + /// Gets the rho indicator + /// + public Rho Rho { get; } + + /// + /// Gets the interest rate used in the calculations + /// + public decimal InterestRate => Delta.RiskFreeRate; + + /// + /// Gets the dividend yield used in the calculations + /// + public decimal DividendYield => Delta.DividendYield; + + /// + /// Gets the current greeks values + /// + public Greeks Greeks => new GreeksHolder(Delta, Gamma, Vega, Theta, Rho); + + /// + /// Creates a new instance of the class + /// + public GreeksIndicators(Symbol optionSymbol, Symbol mirrorOptionSymbol, OptionPricingModelType? optionModel = null, + OptionPricingModelType? ivModel = null) + { + _optionSymbol = optionSymbol; + _mirrorOptionSymbol = mirrorOptionSymbol; + + IDividendYieldModel dividendYieldModel = optionSymbol.SecurityType != SecurityType.IndexOption + ? DividendYieldProvider.CreateForOption(_optionSymbol) + : new ConstantDividendYieldModel(0); + + ImpliedVolatility = new ImpliedVolatility(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, ivModel); + Delta = new Delta(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + Gamma = new Gamma(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + Vega = new Vega(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + Theta = new Theta(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + Rho = new Rho(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + + Delta.ImpliedVolatility = ImpliedVolatility; + Gamma.ImpliedVolatility = ImpliedVolatility; + Vega.ImpliedVolatility = ImpliedVolatility; + Theta.ImpliedVolatility = ImpliedVolatility; + Rho.ImpliedVolatility = ImpliedVolatility; + } + + /// + /// Feeds the specified data into the indicators + /// + public void Update(IBaseData data) + { + ImpliedVolatility.Update(data); + Delta.Update(data); + Gamma.Update(data); + Vega.Update(data); + Theta.Update(data); + Rho.Update(data); + } + + private class GreeksHolder : Greeks + { + public override decimal Delta { get; } + + public override decimal Gamma { get; } + + public override decimal Vega { get; } + + public override decimal Theta { get; } + + public override decimal Rho { get; } + + public override decimal Lambda { get; } + + public GreeksHolder(decimal delta, decimal gamma, decimal vega, decimal theta, decimal rho) + { + Delta = delta; + Gamma = gamma; + Vega = vega; + Theta = theta; + Rho = rho; + } + } + } +} diff --git a/Indicators/ImpliedVolatility.cs b/Indicators/ImpliedVolatility.cs index 18aacc103c7f..d18b0e7bdd13 100644 --- a/Indicators/ImpliedVolatility.cs +++ b/Indicators/ImpliedVolatility.cs @@ -72,10 +72,24 @@ public ImpliedVolatility(string name, Symbol option, IRiskFreeInterestRateModel TheoreticalPrice = new FunctionalIndicator($"{name}_TheoreticalPrice", (iv) => { + // Volatility is zero, price is not changing, can return current theoretical price. + // This also allows us avoid errors in calculation when IV is zero. + if (iv.Value == 0m) + { + return TheoreticalPrice.Current.Value; + } + var theoreticalPrice = CalculateTheoreticalPrice((double)iv.Value, (double)UnderlyingPrice.Current.Value, (double)Strike, OptionGreekIndicatorsHelper.TimeTillExpiry(Expiry, iv.EndTime), (double)RiskFreeRate.Current.Value, (double)DividendYield.Current.Value, Right, optionModel); - return Convert.ToDecimal(theoreticalPrice); + try + { + return Convert.ToDecimal(theoreticalPrice); + } + catch (OverflowException) + { + return TheoreticalPrice.Current.Value; + } }, _ => IsReady) .Of(this); @@ -253,6 +267,15 @@ protected override decimal ComputeIndicator() return _impliedVolatility; } + /// + /// Resets this indicator and all sub-indicators + /// + public override void Reset() + { + TheoreticalPrice.Reset(); + base.Reset(); + } + // Calculate the theoretical option price private static double CalculateTheoreticalPrice(double volatility, double spotPrice, double strikePrice, double timeTillExpiry, double riskFreeRate, double dividendYield, OptionRight optionType, OptionPricingModelType? optionModel = null) diff --git a/Indicators/IndicatorBasedOptionPriceModel.cs b/Indicators/IndicatorBasedOptionPriceModel.cs index 6171e7f02982..651c10f2c7fe 100644 --- a/Indicators/IndicatorBasedOptionPriceModel.cs +++ b/Indicators/IndicatorBasedOptionPriceModel.cs @@ -137,124 +137,4 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon () => greeksIndicators.Value.Greeks); } } - - /// - /// Helper class that holds and updates the greeks indicators - /// - public class GreeksIndicators - { - private readonly static IRiskFreeInterestRateModel _interestRateProvider = new InterestRateProvider(); - - private readonly Symbol _optionSymbol; - private readonly Symbol _mirrorOptionSymbol; - - /// - /// Gets the implied volatility indicator - /// - public ImpliedVolatility ImpliedVolatility { get; } - - /// - /// Gets the delta indicator - /// - public Delta Delta { get; } - - /// - /// Gets the gamma indicator - /// - public Gamma Gamma { get; } - - /// - /// Gets the vega indicator - /// - public Vega Vega { get; } - - /// - /// Gets the theta indicator - /// - public Theta Theta { get; } - - /// - /// Gets the rho indicator - /// - public Rho Rho { get; } - - /// - /// Gets the interest rate used in the calculations - /// - public decimal InterestRate => Delta.RiskFreeRate; - - /// - /// Gets the dividend yield used in the calculations - /// - public decimal DividendYield => Delta.DividendYield; - - /// - /// Gets the current greeks values - /// - public Greeks Greeks => new GreeksHolder(Delta, Gamma, Vega, Theta, Rho); - - /// - /// Creates a new instance of the class - /// - public GreeksIndicators(Symbol optionSymbol, Symbol mirrorOptionSymbol, OptionPricingModelType? optionModel = null, - OptionPricingModelType? ivModel = null) - { - _optionSymbol = optionSymbol; - _mirrorOptionSymbol = mirrorOptionSymbol; - - IDividendYieldModel dividendYieldModel = optionSymbol.SecurityType != SecurityType.IndexOption - ? DividendYieldProvider.CreateForOption(_optionSymbol) - : new ConstantDividendYieldModel(0); - - ImpliedVolatility = new ImpliedVolatility(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, ivModel); - Delta = new Delta(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); - Gamma = new Gamma(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); - Vega = new Vega(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); - Theta = new Theta(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); - Rho = new Rho(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); - - Delta.ImpliedVolatility = ImpliedVolatility; - Gamma.ImpliedVolatility = ImpliedVolatility; - Vega.ImpliedVolatility = ImpliedVolatility; - Theta.ImpliedVolatility = ImpliedVolatility; - Rho.ImpliedVolatility = ImpliedVolatility; - } - - /// - /// Feeds the specified data into the indicators - /// - public void Update(IBaseData data) - { - ImpliedVolatility.Update(data); - Delta.Update(data); - Gamma.Update(data); - Vega.Update(data); - Theta.Update(data); - Rho.Update(data); - } - - private class GreeksHolder : Greeks - { - public override decimal Delta { get; } - - public override decimal Gamma { get; } - - public override decimal Vega { get; } - - public override decimal Theta { get; } - - public override decimal Rho { get; } - - public override decimal Lambda { get; } - - public GreeksHolder(decimal delta, decimal gamma, decimal vega, decimal theta, decimal rho) - { - Delta = delta; - Gamma = gamma; - Vega = vega; - Theta = theta; - Rho = rho; - } - } - } }