From 6b07021f19c6304f152ab50fc3b596e6e2477731 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 23 Jan 2026 13:37:00 -0400 Subject: [PATCH 01/18] 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 | 26 +- Indicators/ImpliedVolatility.cs | 22 +- Indicators/IndicatorBasedOptionPriceModel.cs | 260 ++++++++++++++++++ .../Securities/OptionPriceModelTests.cs | 2 +- .../IndicatorBasedOptionPriceModelTests.cs | 128 +++++++++ 7 files changed, 652 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 46e9eaa0c70a..db2708f41571 100644 --- a/Common/Securities/Option/OptionPriceModelResult.cs +++ b/Common/Securities/Option/OptionPriceModelResult.cs @@ -29,13 +29,20 @@ public class OptionPriceModelResult /// public static OptionPriceModelResult None { get; } = new(0, NullGreeks.Instance); + private Lazy _theoreticalPrice; private Lazy _greeks; private Lazy _impliedVolatility; /// /// Gets the theoretical price as computed by the /// - public decimal TheoreticalPrice { get; set; } + public decimal TheoreticalPrice + { + get + { + return _theoreticalPrice.Value; + } + } /// /// Gets the implied volatility of the option contract @@ -80,10 +87,8 @@ public OptionPriceModelResult() /// 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); } /// @@ -104,8 +109,19 @@ public OptionPriceModelResult(decimal theoreticalPrice, decimal impliedVolatilit /// 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 9098c927c0bf..b741bed921b0 100644 --- a/Tests/Common/Securities/OptionPriceModelTests.cs +++ b/Tests/Common/Securities/OptionPriceModelTests.cs @@ -997,7 +997,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 059d494b8c27351696ecf223cf2d715ddb8593fb Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 23 Jan 2026 16:55:52 -0400 Subject: [PATCH 02/18] 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; - } - } - } } From 30d8d1903064d0cd3332c86975c7d21137a809df Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 3 Feb 2026 12:21:06 -0400 Subject: [PATCH 03/18] Address peer review --- Common/Extensions.cs | 21 ++++++++++ Common/Messages/Messages.QuantConnect.cs | 5 +++ Indicators/GreeksIndicators.cs | 4 +- Indicators/IndicatorBasedOptionPriceModel.cs | 22 +++++++---- Tests/Common/Util/ExtensionsTests.cs | 40 ++++++++++++++++++++ 5 files changed, 82 insertions(+), 10 deletions(-) diff --git a/Common/Extensions.cs b/Common/Extensions.cs index f34acb4b53c6..88343d1d5a27 100644 --- a/Common/Extensions.cs +++ b/Common/Extensions.cs @@ -3612,6 +3612,27 @@ public static Symbol AdjustSymbolByOffset(this Symbol symbol, uint offset) return symbol; } + /// + /// Helper method to get the mirror option symbol for a given option symbol + /// + /// The original option contract symbol + /// The mirror option contract symbol + public static Symbol GetMirrorOptionSymbol(this Symbol contractSymbol) + { + if (!contractSymbol.SecurityType.IsOption() || contractSymbol.IsCanonical()) + { + throw new ArgumentException(Messages.Extensions.NotAValidOptionSymbolForMirror); + } + + return Symbol.CreateOption(contractSymbol.Underlying, + contractSymbol.ID.Symbol, + contractSymbol.ID.Market, + contractSymbol.ID.OptionStyle, + contractSymbol.ID.OptionRight.Invert(), + contractSymbol.ID.StrikePrice, + contractSymbol.ID.Date); + } + /// /// Helper method to unsubscribe a given configuration, handling any required mapping /// diff --git a/Common/Messages/Messages.QuantConnect.cs b/Common/Messages/Messages.QuantConnect.cs index d0aaf4fecfb3..9dec38803b95 100644 --- a/Common/Messages/Messages.QuantConnect.cs +++ b/Common/Messages/Messages.QuantConnect.cs @@ -256,6 +256,11 @@ public static class Extensions /// public static string GreatestCommonDivisorEmptyList = "The list of values cannot be empty"; + /// + /// Returns a string message saying that the symbol for which a mirror contract is being created is not a valid option symbol + /// + public static string NotAValidOptionSymbolForMirror = "Cannot create mirror contract for non-option symbol or canonical option symbol"; + /// /// Returns a string message saying the process of downloading data from the given url failed /// diff --git a/Indicators/GreeksIndicators.cs b/Indicators/GreeksIndicators.cs index c751f81c8d30..ea2ba288c00f 100644 --- a/Indicators/GreeksIndicators.cs +++ b/Indicators/GreeksIndicators.cs @@ -36,12 +36,12 @@ public class GreeksIndicators /// /// Gets the delta indicator /// - public Delta Delta { get; } + public Delta Delta { get; } /// /// Gets the gamma indicator /// - public Gamma Gamma { get; } + public Gamma Gamma { get; } /// /// Gets the vega indicator diff --git a/Indicators/IndicatorBasedOptionPriceModel.cs b/Indicators/IndicatorBasedOptionPriceModel.cs index 651c10f2c7fe..9b41d0a65495 100644 --- a/Indicators/IndicatorBasedOptionPriceModel.cs +++ b/Indicators/IndicatorBasedOptionPriceModel.cs @@ -29,6 +29,9 @@ namespace QuantConnect.Indicators /// public class IndicatorBasedOptionPriceModel : IOptionPriceModel { + private Symbol _contractSymbol; + private Symbol _mirrorContractSymbol; + /// /// Creates a new containing the theoretical price based on /// QuantConnect indicators. @@ -55,7 +58,15 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon return OptionPriceModelResult.None; } - var contractSymbol = contract.Symbol; + var contractSymbol = _contractSymbol; + // These models are supposed to be one per contract (security instance), so we cache the symbols to avoid calling + // GetMirrorOptionSymbol multiple times. If the contract changes by any reason, we just update the cached symbols. + if (contractSymbol != contract.Symbol) + { + contractSymbol = _contractSymbol = contract.Symbol; + _mirrorContractSymbol = contractSymbol.GetMirrorOptionSymbol(); + } + 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))) @@ -88,13 +99,7 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon 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 mirrorContractSymbol = _mirrorContractSymbol; var mirrorOptionData = slice.AllData .Where(x => x.Symbol == mirrorContractSymbol && // Use the same resolution data @@ -108,6 +113,7 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon { Log.Debug($"IndicatorBasedOptionPriceModel.Evaluate(). Missing data for mirror option {mirrorContractSymbol}. Using contract symbol only."); } + // Null so that the indicators don't consider the mirror option and don't expect data for it mirrorContractSymbol = null; } diff --git a/Tests/Common/Util/ExtensionsTests.cs b/Tests/Common/Util/ExtensionsTests.cs index 83088bcc0479..696511e1bf21 100644 --- a/Tests/Common/Util/ExtensionsTests.cs +++ b/Tests/Common/Util/ExtensionsTests.cs @@ -46,6 +46,7 @@ using QuantConnect.Scheduling; using QuantConnect.Securities; using QuantConnect.Tests.Brokerages; +using QuantConnect.Util; namespace QuantConnect.Tests.Common.Util { @@ -2234,6 +2235,45 @@ private static HttpClient MockClient(string content, HttpStatusCode code) return new HttpClient(handlerMock.Object); } + private static TestCaseData[] MirrorOptionTestCases + { + get + { + var spy = Symbol.Create("SPY", SecurityType.Equity, Market.USA); + var spx = Symbol.Create("SPX", SecurityType.Index, Market.USA); + + var strike = 100m; + var expiry = new DateTime(2021, 1, 1); + + var spyCall = Symbol.CreateOption(spy, Market.USA, OptionStyle.American, OptionRight.Call, strike, expiry); + var spyPut = Symbol.CreateOption(spy, Market.USA, OptionStyle.American, OptionRight.Put, strike, expiry); + + var spxCall = Symbol.CreateOption(spx, Market.USA, OptionStyle.European, OptionRight.Call, strike, expiry); + var spxPut = Symbol.CreateOption(spx, Market.USA, OptionStyle.European, OptionRight.Put, strike, expiry); + + var spxwCall = Symbol.CreateOption(spx, "SPXW", Market.USA, OptionStyle.European, OptionRight.Call, strike, expiry); + var spxwPut = Symbol.CreateOption(spx, "SPXW", Market.USA, OptionStyle.European, OptionRight.Put, strike, expiry); + + return new[] + { + new TestCaseData(spyCall).Returns(spyPut), + new TestCaseData(spyPut).Returns(spyCall), + + new TestCaseData(spxCall).Returns(spxPut), + new TestCaseData(spxPut).Returns(spxCall), + + new TestCaseData(spxwCall).Returns(spxwPut), + new TestCaseData(spxwPut).Returns(spxwCall), + }; + } + } + + [TestCaseSource(nameof(MirrorOptionTestCases))] + public Symbol GetsCorrectMirrorOption(Symbol optionSymbol) + { + return optionSymbol.GetMirrorOptionSymbol(); + } + private PyObject ConvertToPyObject(object value) { using (Py.GIL()) From b70e866e9fbf74aaffb16a64b785b1dfda0558bf Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 3 Feb 2026 18:11:03 -0400 Subject: [PATCH 04/18] Minor tests fixes --- ...torBasedOptionPricingModelRegressionAlgorithm.cs | 13 ++++++++++--- Tests/Algorithm/AlgorithmIndicatorsTests.cs | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs b/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs index 24c495dbcd32..1917c1f44c2a 100644 --- a/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs +++ b/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs @@ -75,9 +75,16 @@ public override void OnData(Slice slice) $"Theta: {greeks.Theta}, Rho: {greeks.Rho}, Lambda: {greeks.Lambda}"); // Sanity check values - if (theoreticalPrice <= 0) + + var theoreticalPriceChecked = false; + // If IV is zero (model could not converge) we skip the theoretical price check, as it will be zero too + if (iv != 0) { - throw new RegressionTestException($"Invalid theoretical price for {contract.Symbol}: {theoreticalPrice}"); + if (theoreticalPrice <= 0) + { + throw new RegressionTestException($"Invalid theoretical price for {contract.Symbol}: {theoreticalPrice}"); + } + theoreticalPriceChecked = true; } // 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 || @@ -101,7 +108,7 @@ public override void OnData(Slice slice) throw new RegressionTestException($"EvaluatePriceModel returned different results for {contract.Symbol}"); } - _checked |= true; + _checked |= theoreticalPriceChecked; } } } diff --git a/Tests/Algorithm/AlgorithmIndicatorsTests.cs b/Tests/Algorithm/AlgorithmIndicatorsTests.cs index e5636f9e34e7..dd0764671c4d 100644 --- a/Tests/Algorithm/AlgorithmIndicatorsTests.cs +++ b/Tests/Algorithm/AlgorithmIndicatorsTests.cs @@ -524,7 +524,7 @@ public void IndicatorHistoryIsSupportedInPythonForOptionsIndicators([Range(1, 4) Assert.AreEqual(390, dataFrame.GetAttr("shape")[0].GetAndDispose()); // Assert dataframe column names are current, price, oppositeprice and underlyingprice var columns = dataFrame.GetAttr("columns").InvokeMethod>("tolist"); - var expectedColumns = new[] { "current", "price", "oppositeprice", "underlyingprice" }; + var expectedColumns = new[] { "current", "price", "oppositeprice", "underlyingprice", "theoreticalprice" }; CollectionAssert.AreEquivalent(expectedColumns, columns); } From 766ecf8c198cb5c73eae92d8cc4a7c80ddf32385 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 4 Feb 2026 10:36:43 -0400 Subject: [PATCH 05/18] Make the indicator based price model the default for options --- .../OptionGreeksRegressionAlgorithm.cs | 2 +- .../Securities/FutureOption/FutureOption.cs | 3 +- Common/Securities/IndexOption/IndexOption.cs | 7 ++- .../Option/IOptionPriceModelProvider.cs | 30 ++++++++++++ Common/Securities/Option/Option.cs | 32 ++++++------- .../Option/QLOptionPriceModelProvider.cs | 46 +++++++++++++++++++ Common/Securities/SecurityService.cs | 10 ++-- Engine/Engine.cs | 4 +- .../IndicatorBasedOptionPriceModelProvider.cs | 35 ++++++++++++++ 9 files changed, 143 insertions(+), 26 deletions(-) create mode 100644 Common/Securities/Option/IOptionPriceModelProvider.cs create mode 100644 Common/Securities/Option/QLOptionPriceModelProvider.cs create mode 100644 Indicators/IndicatorBasedOptionPriceModelProvider.cs diff --git a/Algorithm.CSharp/OptionGreeksRegressionAlgorithm.cs b/Algorithm.CSharp/OptionGreeksRegressionAlgorithm.cs index c0b5108fdc87..5441e873cea7 100644 --- a/Algorithm.CSharp/OptionGreeksRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionGreeksRegressionAlgorithm.cs @@ -27,7 +27,7 @@ namespace QuantConnect.Algorithm.CSharp public class OptionGreeksRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition { private Symbol _itmCallSymbol, _otmCallSymbol, _itmPutSymbol, _otmPutSymbol; - private const decimal error = 0.05m; + private const decimal error = 0.1m; public override void Initialize() { diff --git a/Common/Securities/FutureOption/FutureOption.cs b/Common/Securities/FutureOption/FutureOption.cs index b08c9e66c8bc..e09598c7e82d 100644 --- a/Common/Securities/FutureOption/FutureOption.cs +++ b/Common/Securities/FutureOption/FutureOption.cs @@ -60,7 +60,8 @@ public FutureOption(Symbol symbol, new SecurityPriceVariationModel(), currencyConverter, registeredTypes, - underlying + underlying, + null ) { BuyingPowerModel = new FuturesOptionsMarginModel(0, this); diff --git a/Common/Securities/IndexOption/IndexOption.cs b/Common/Securities/IndexOption/IndexOption.cs index 3347018f04a5..e23f91ba7246 100644 --- a/Common/Securities/IndexOption/IndexOption.cs +++ b/Common/Securities/IndexOption/IndexOption.cs @@ -38,6 +38,7 @@ public class IndexOption : Option.Option /// Cache of security objects /// Future underlying security /// Settlement type for the index option. Most index options are cash-settled. + /// The option price model provider public IndexOption(Symbol symbol, SecurityExchangeHours exchangeHours, Cash quoteCurrency, @@ -46,7 +47,8 @@ public IndexOption(Symbol symbol, IRegisteredSecurityDataTypesProvider registeredTypes, SecurityCache securityCache, Security underlying, - SettlementType settlementType = SettlementType.Cash) + SettlementType settlementType = SettlementType.Cash, + IOptionPriceModelProvider priceModelProvider = null) : base(symbol, quoteCurrency, symbolProperties, @@ -63,7 +65,8 @@ public IndexOption(Symbol symbol, new IndexOptionPriceVariationModel(), currencyConverter, registeredTypes, - underlying + underlying, + priceModelProvider ) { ExerciseSettlement = settlementType; diff --git a/Common/Securities/Option/IOptionPriceModelProvider.cs b/Common/Securities/Option/IOptionPriceModelProvider.cs new file mode 100644 index 000000000000..a13ebfc9ebde --- /dev/null +++ b/Common/Securities/Option/IOptionPriceModelProvider.cs @@ -0,0 +1,30 @@ +/* + * 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. +*/ + +namespace QuantConnect.Securities.Option +{ + /// + /// Provides option price models for option securities + /// + public interface IOptionPriceModelProvider + { + /// + /// Gets the option price model for the specified option symbol + /// + /// The symbol + /// The option price model for the given symbol + IOptionPriceModel GetOptionPriceModel(Symbol symbol); + } +} diff --git a/Common/Securities/Option/Option.cs b/Common/Securities/Option/Option.cs index 81ea3aaf7634..5b3a51dd82ff 100644 --- a/Common/Securities/Option/Option.cs +++ b/Common/Securities/Option/Option.cs @@ -57,13 +57,15 @@ public class Option : Security, IDerivativeSecurity, IOptionPrice /// Currency converter used to convert /// instances into units of the account currency /// Provides all data types registered in the algorithm + /// The option price model provider /// Used in testing public Option(SecurityExchangeHours exchangeHours, SubscriptionDataConfig config, Cash quoteCurrency, OptionSymbolProperties symbolProperties, ICurrencyConverter currencyConverter, - IRegisteredSecurityDataTypesProvider registeredTypes) + IRegisteredSecurityDataTypesProvider registeredTypes, + IOptionPriceModelProvider priceModelProvider = null) : this(config.Symbol, quoteCurrency, symbolProperties, @@ -80,7 +82,8 @@ public Option(SecurityExchangeHours exchangeHours, new SecurityPriceVariationModel(), currencyConverter, registeredTypes, - null) + null, + priceModelProvider) { AddData(config); SetDataNormalizationMode(DataNormalizationMode.Raw); @@ -98,6 +101,7 @@ public Option(SecurityExchangeHours exchangeHours, /// Provides all data types registered in the algorithm /// Cache to store security information /// Future underlying security + /// The option price model provider public Option(Symbol symbol, SecurityExchangeHours exchangeHours, Cash quoteCurrency, @@ -105,8 +109,9 @@ public Option(Symbol symbol, ICurrencyConverter currencyConverter, IRegisteredSecurityDataTypesProvider registeredTypes, SecurityCache securityCache, - Security underlying) - : this(symbol, + Security underlying, + IOptionPriceModelProvider priceModelProvider = null) + : this (symbol, quoteCurrency, symbolProperties, new OptionExchange(exchangeHours), @@ -122,7 +127,8 @@ public Option(Symbol symbol, new SecurityPriceVariationModel(), currencyConverter, registeredTypes, - underlying) + underlying, + priceModelProvider) { } @@ -149,7 +155,8 @@ protected Option(Symbol symbol, IPriceVariationModel priceVariationModel, ICurrencyConverter currencyConverter, IRegisteredSecurityDataTypesProvider registeredTypesProvider, - Security underlying + Security underlying, + IOptionPriceModelProvider priceModelProvider ) : base( symbol, quoteCurrency, @@ -173,18 +180,7 @@ Security underlying ExerciseSettlement = SettlementType.PhysicalDelivery; SetDataNormalizationMode(DataNormalizationMode.Raw); OptionExerciseModel = new DefaultExerciseModel(); - PriceModel = symbol.ID.OptionStyle switch - { - // CRR model has the best accuracy and speed suggested by - // Branka, Zdravka & Tea (2014). Numerical Methods versus Bjerksund and Stensland Approximations for American Options Pricing. - // International Journal of Economics and Management Engineering. 8:4. - // Available via: https://downloads.dxfeed.com/specifications/dxLibOptions/Numerical-Methods-versus-Bjerksund-and-Stensland-Approximations-for-American-Options-Pricing-.pdf - // Also refer to OptionPriceModelTests.MatchesIBGreeksBulk() test, - // we select the most accurate and computational efficient model - OptionStyle.American => OptionPriceModels.BinomialCoxRossRubinstein(), - OptionStyle.European => OptionPriceModels.BlackScholes(), - _ => throw new ArgumentException("Invalid OptionStyle") - }; + PriceModel = (priceModelProvider ?? new QLOptionPriceModelProvider()).GetOptionPriceModel(symbol); Holdings = new OptionHolding(this, currencyConverter); _symbolProperties = (OptionSymbolProperties)symbolProperties; SetFilter(-1, 1, TimeSpan.Zero, TimeSpan.FromDays(35)); diff --git a/Common/Securities/Option/QLOptionPriceModelProvider.cs b/Common/Securities/Option/QLOptionPriceModelProvider.cs new file mode 100644 index 000000000000..95e84586888a --- /dev/null +++ b/Common/Securities/Option/QLOptionPriceModelProvider.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 System; + +namespace QuantConnect.Securities.Option +{ + /// + /// Provides option price models for option securities based on QuantLib implementations + /// + public class QLOptionPriceModelProvider : IOptionPriceModelProvider + { + /// + /// Gets the option price model for the specified option symbol + /// + /// The symbol + /// The option price model for the given symbol + public IOptionPriceModel GetOptionPriceModel(Symbol symbol) + { + return symbol.ID.OptionStyle switch + { + // CRR model has the best accuracy and speed suggested by + // Branka, Zdravka & Tea (2014). Numerical Methods versus Bjerksund and Stensland Approximations for American Options Pricing. + // International Journal of Economics and Management Engineering. 8:4. + // Available via: https://downloads.dxfeed.com/specifications/dxLibOptions/Numerical-Methods-versus-Bjerksund-and-Stensland-Approximations-for-American-Options-Pricing-.pdf + // Also refer to OptionPriceModelTests.MatchesIBGreeksBulk() test, + // we select the most accurate and computational efficient model + OptionStyle.American => OptionPriceModels.BinomialCoxRossRubinstein(), + OptionStyle.European => OptionPriceModels.BlackScholes(), + _ => throw new ArgumentException("Invalid OptionStyle") + }; + } + } +} diff --git a/Common/Securities/SecurityService.cs b/Common/Securities/SecurityService.cs index 1cfd4e77cc7a..b0ad767881e4 100644 --- a/Common/Securities/SecurityService.cs +++ b/Common/Securities/SecurityService.cs @@ -21,6 +21,7 @@ using QuantConnect.Interfaces; using System.Collections.Generic; using QuantConnect.Data.Market; +using QuantConnect.Securities.Option; namespace QuantConnect.Securities { @@ -36,6 +37,7 @@ public class SecurityService : ISecurityService private readonly ISecurityInitializerProvider _securityInitializerProvider; private readonly SecurityCacheProvider _cacheProvider; private readonly IPrimaryExchangeProvider _primaryExchangeProvider; + private readonly IOptionPriceModelProvider _optionPriceModelProvider; private readonly IAlgorithm _algorithm; private bool _isLiveMode; private bool _modelsMismatchWarningSent; @@ -50,7 +52,8 @@ public SecurityService(CashBook cashBook, IRegisteredSecurityDataTypesProvider registeredTypes, SecurityCacheProvider cacheProvider, IPrimaryExchangeProvider primaryExchangeProvider = null, - IAlgorithm algorithm = null) + IAlgorithm algorithm = null, + IOptionPriceModelProvider optionPriceModelProvider = null) { _cashBook = cashBook; _registeredTypes = registeredTypes; @@ -60,6 +63,7 @@ public SecurityService(CashBook cashBook, _cacheProvider = cacheProvider; _primaryExchangeProvider = primaryExchangeProvider; _algorithm = algorithm; + _optionPriceModelProvider = optionPriceModelProvider; } /// @@ -176,12 +180,12 @@ private Security CreateSecurity(Symbol symbol, case SecurityType.Option: if (addToSymbolCache) SymbolCache.Set(symbol.Underlying.Value, symbol.Underlying); - security = new Option.Option(symbol, exchangeHours, quoteCash, new Option.OptionSymbolProperties(symbolProperties), _cashBook, _registeredTypes, cache, underlying); + security = new Option.Option(symbol, exchangeHours, quoteCash, new Option.OptionSymbolProperties(symbolProperties), _cashBook, _registeredTypes, cache, underlying, _optionPriceModelProvider); break; case SecurityType.IndexOption: if (addToSymbolCache) SymbolCache.Set(symbol.Underlying.Value, symbol.Underlying); - security = new IndexOption.IndexOption(symbol, exchangeHours, quoteCash, new IndexOption.IndexOptionSymbolProperties(symbolProperties), _cashBook, _registeredTypes, cache, underlying); + security = new IndexOption.IndexOption(symbol, exchangeHours, quoteCash, new IndexOption.IndexOptionSymbolProperties(symbolProperties), _cashBook, _registeredTypes, cache, underlying, priceModelProvider: _optionPriceModelProvider); break; case SecurityType.FutureOption: diff --git a/Engine/Engine.cs b/Engine/Engine.cs index 5bd6209d0afc..9e9bfbdc820b 100644 --- a/Engine/Engine.cs +++ b/Engine/Engine.cs @@ -26,6 +26,7 @@ using QuantConnect.Data; using QuantConnect.Data.Auxiliary; using QuantConnect.Exceptions; +using QuantConnect.Indicators; using QuantConnect.Interfaces; using QuantConnect.Lean.Engine.DataFeeds; using QuantConnect.Lean.Engine.HistoricalData; @@ -151,7 +152,8 @@ public void Run(AlgorithmNodePacket job, AlgorithmManager manager, string assemb registeredTypesProvider, new SecurityCacheProvider(algorithm.Portfolio), mapFilePrimaryExchangeProvider, - algorithm); + algorithm, + new IndicatorBasedOptionPriceModelProvider()); algorithm.Securities.SetSecurityService(securityService); diff --git a/Indicators/IndicatorBasedOptionPriceModelProvider.cs b/Indicators/IndicatorBasedOptionPriceModelProvider.cs new file mode 100644 index 000000000000..86552c5b9dee --- /dev/null +++ b/Indicators/IndicatorBasedOptionPriceModelProvider.cs @@ -0,0 +1,35 @@ +/* + * 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.Securities.Option; + +namespace QuantConnect.Indicators +{ + /// + /// Provides option price models for option securities based on Lean's Greeks indicators + /// + public class IndicatorBasedOptionPriceModelProvider : IOptionPriceModelProvider + { + /// + /// Gets the option price model for the specified option symbol + /// + /// The symbol + /// The option price model for the given symbol + public IOptionPriceModel GetOptionPriceModel(Symbol symbol) + { + return new IndicatorBasedOptionPriceModel(); + } + } +} From 2013229f7cd56331a790e54cbbab639ede8431c2 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 5 Feb 2026 13:38:17 -0400 Subject: [PATCH 06/18] Address peer review --- ...edOptionPricingModelRegressionAlgorithm.cs | 6 +- Common/Securities/Option/Option.cs | 2 +- .../Option/QLOptionPriceModelProvider.cs | 9 ++ Engine/Engine.cs | 2 +- Indicators/IndicatorBasedOptionPriceModel.cs | 88 +++++++++++++------ .../IndicatorBasedOptionPriceModelProvider.cs | 9 ++ 6 files changed, 85 insertions(+), 31 deletions(-) diff --git a/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs b/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs index 1917c1f44c2a..0c19ccf3e6e2 100644 --- a/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs +++ b/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs @@ -44,7 +44,11 @@ public override void Initialize() SetCash(100000); _option = GetOption(); - _option.PriceModel = new IndicatorBasedOptionPriceModel(); + + if (_option.PriceModel is not IndicatorBasedOptionPriceModel) + { + throw new RegressionTestException("Option pricing model was not set to IndicatorBasedOptionPriceModel, which should be the default"); + } } protected virtual Option GetOption() diff --git a/Common/Securities/Option/Option.cs b/Common/Securities/Option/Option.cs index 5b3a51dd82ff..38504b6e0e51 100644 --- a/Common/Securities/Option/Option.cs +++ b/Common/Securities/Option/Option.cs @@ -180,7 +180,7 @@ IOptionPriceModelProvider priceModelProvider ExerciseSettlement = SettlementType.PhysicalDelivery; SetDataNormalizationMode(DataNormalizationMode.Raw); OptionExerciseModel = new DefaultExerciseModel(); - PriceModel = (priceModelProvider ?? new QLOptionPriceModelProvider()).GetOptionPriceModel(symbol); + PriceModel = (priceModelProvider ?? QLOptionPriceModelProvider.Instance).GetOptionPriceModel(symbol); Holdings = new OptionHolding(this, currencyConverter); _symbolProperties = (OptionSymbolProperties)symbolProperties; SetFilter(-1, 1, TimeSpan.Zero, TimeSpan.FromDays(35)); diff --git a/Common/Securities/Option/QLOptionPriceModelProvider.cs b/Common/Securities/Option/QLOptionPriceModelProvider.cs index 95e84586888a..b43444137b9d 100644 --- a/Common/Securities/Option/QLOptionPriceModelProvider.cs +++ b/Common/Securities/Option/QLOptionPriceModelProvider.cs @@ -22,6 +22,15 @@ namespace QuantConnect.Securities.Option /// public class QLOptionPriceModelProvider : IOptionPriceModelProvider { + /// + /// Singleton instance of the + /// + public static QLOptionPriceModelProvider Instance { get; } = new(); + + private QLOptionPriceModelProvider() + { + } + /// /// Gets the option price model for the specified option symbol /// diff --git a/Engine/Engine.cs b/Engine/Engine.cs index 9e9bfbdc820b..97f7f465b20f 100644 --- a/Engine/Engine.cs +++ b/Engine/Engine.cs @@ -153,7 +153,7 @@ public void Run(AlgorithmNodePacket job, AlgorithmManager manager, string assemb new SecurityCacheProvider(algorithm.Portfolio), mapFilePrimaryExchangeProvider, algorithm, - new IndicatorBasedOptionPriceModelProvider()); + IndicatorBasedOptionPriceModelProvider.Instance); algorithm.Securities.SetSecurityService(securityService); diff --git a/Indicators/IndicatorBasedOptionPriceModel.cs b/Indicators/IndicatorBasedOptionPriceModel.cs index 9b41d0a65495..69f9af2fe8ab 100644 --- a/Indicators/IndicatorBasedOptionPriceModel.cs +++ b/Indicators/IndicatorBasedOptionPriceModel.cs @@ -18,6 +18,7 @@ using QuantConnect.Logging; using QuantConnect.Securities; using QuantConnect.Securities.Option; +using QuantConnect.Util; using System; using System.Linq; @@ -59,35 +60,74 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon } var contractSymbol = _contractSymbol; + var mirrorContractSymbol = _mirrorContractSymbol; // These models are supposed to be one per contract (security instance), so we cache the symbols to avoid calling // GetMirrorOptionSymbol multiple times. If the contract changes by any reason, we just update the cached symbols. if (contractSymbol != contract.Symbol) { contractSymbol = _contractSymbol = contract.Symbol; - _mirrorContractSymbol = contractSymbol.GetMirrorOptionSymbol(); + mirrorContractSymbol = _mirrorContractSymbol = contractSymbol.GetMirrorOptionSymbol(); } - 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 underlyingData = null; BaseData optionData = null; - if (underlyingData != null) + BaseData mirrorOptionData = null; + + foreach (var underlyingDataType in new[] { typeof(TradeBar), typeof(Tick) }) { - 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 (underlyingDataType == typeof(TradeBar)) + { + if (slice.Bars.TryGetValue(contractSymbol.Underlying, out var underlyingTradeBar) && + slice.QuoteBars.TryGetValue(contractSymbol, out var optionQuoteBar) && + underlyingTradeBar.Period == optionQuoteBar.Period) + { + underlyingData = underlyingTradeBar; + optionData = optionQuoteBar; + + if (slice.QuoteBars.TryGetValue(mirrorContractSymbol, out var mirrorOptionQuoteBar) && + mirrorOptionQuoteBar.Period == underlyingTradeBar.Period) + { + mirrorOptionData = mirrorOptionQuoteBar; + } + + break; + } + } + else + { + if (slice.Ticks.TryGetValue(contractSymbol.Underlying, out var underlyingTicks) && + slice.Ticks.TryGetValue(contractSymbol, out var optionTicks)) + { + // Get last underlying trade tick + underlyingData = underlyingTicks + .Where(x => x.TickType == TickType.Trade) + .LastOrDefault(); + if (underlyingData == null) + { + continue; + } + + // Get last option quote tick + optionData = optionTicks + .Where(x => x.TickType == TickType.Quote) + .LastOrDefault(); + if (optionData == null) + { + underlyingData = null; + continue; + } + + // Try to get last mirror option quote tick + if (slice.Ticks.TryGetValue(_mirrorContractSymbol, out var mirrorOptionTicks)) + { + mirrorOptionData = mirrorOptionTicks + .Where(x => x.TickType == TickType.Quote) + .LastOrDefault(); + } + + break; + } + } } if (underlyingData == null || optionData == null) @@ -99,14 +139,6 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon return OptionPriceModelResult.None; } - var mirrorContractSymbol = _mirrorContractSymbol; - 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) diff --git a/Indicators/IndicatorBasedOptionPriceModelProvider.cs b/Indicators/IndicatorBasedOptionPriceModelProvider.cs index 86552c5b9dee..677b7fb5188e 100644 --- a/Indicators/IndicatorBasedOptionPriceModelProvider.cs +++ b/Indicators/IndicatorBasedOptionPriceModelProvider.cs @@ -22,6 +22,15 @@ namespace QuantConnect.Indicators /// public class IndicatorBasedOptionPriceModelProvider : IOptionPriceModelProvider { + /// + /// Singleton instance of the + /// + public static IndicatorBasedOptionPriceModelProvider Instance { get; } = new(); + + private IndicatorBasedOptionPriceModelProvider() + { + } + /// /// Gets the option price model for the specified option symbol /// From de8546ea18d857b3b82ce8e7de4bf305f707b4af Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 6 Feb 2026 17:35:01 -0400 Subject: [PATCH 07/18] Cleanup and minor changes --- Indicators/GreeksIndicators.cs | 70 ++++++++++++++++++-- Indicators/IndicatorBasedOptionPriceModel.cs | 47 ++++++++----- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/Indicators/GreeksIndicators.cs b/Indicators/GreeksIndicators.cs index ea2ba288c00f..5f28add1fbe4 100644 --- a/Indicators/GreeksIndicators.cs +++ b/Indicators/GreeksIndicators.cs @@ -24,6 +24,7 @@ namespace QuantConnect.Indicators public class GreeksIndicators { private readonly static IRiskFreeInterestRateModel _interestRateProvider = new InterestRateProvider(); + private readonly static IDividendYieldModel _constantDividendYieldModel = new ConstantDividendYieldModel(0); private readonly Symbol _optionSymbol; private readonly Symbol _mirrorOptionSymbol; @@ -73,18 +74,44 @@ public class GreeksIndicators /// public Greeks Greeks => new GreeksHolder(Delta, Gamma, Vega, Theta, Rho); + /// + /// Whether the mirror option is set and will be used in the calculations. + /// + public bool UseMirrorOption => _mirrorOptionSymbol != null; + + /// + /// Gets the current result of the greeks indicators, including the implied volatility, theoretical price and greeks values + /// + public GreeksIndicatorsResult CurrentResult => new GreeksIndicatorsResult + { + ImpliedVolatility = ImpliedVolatility, + TheoreticalPrice = ImpliedVolatility.TheoreticalPrice, + Greeks = Greeks + }; + + /// + /// Gets the dividend yield model to be used in the calculations for the specified option symbol. + /// + public static IDividendYieldModel GetDividendYieldModel(Symbol optionSymbol) + { + return optionSymbol.SecurityType != SecurityType.IndexOption + ? DividendYieldProvider.CreateForOption(optionSymbol) + : _constantDividendYieldModel; + } + /// /// Creates a new instance of the class /// public GreeksIndicators(Symbol optionSymbol, Symbol mirrorOptionSymbol, OptionPricingModelType? optionModel = null, - OptionPricingModelType? ivModel = null) + OptionPricingModelType? ivModel = null, IDividendYieldModel dividendYieldModel = null) { _optionSymbol = optionSymbol; _mirrorOptionSymbol = mirrorOptionSymbol; - IDividendYieldModel dividendYieldModel = optionSymbol.SecurityType != SecurityType.IndexOption - ? DividendYieldProvider.CreateForOption(_optionSymbol) - : new ConstantDividendYieldModel(0); + if (dividendYieldModel == null) + { + dividendYieldModel = GetDividendYieldModel(optionSymbol); + } ImpliedVolatility = new ImpliedVolatility(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, ivModel); Delta = new Delta(_optionSymbol, _interestRateProvider, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); @@ -113,6 +140,19 @@ public void Update(IBaseData data) Rho.Update(data); } + /// + /// Resets the indicators to their default state + /// + public void Reset() + { + ImpliedVolatility.Reset(); + Delta.Reset(); + Gamma.Reset(); + Vega.Reset(); + Theta.Reset(); + Rho.Reset(); + } + private class GreeksHolder : Greeks { public override decimal Delta { get; } @@ -137,4 +177,26 @@ public GreeksHolder(decimal delta, decimal gamma, decimal vega, decimal theta, d } } } + + /// + /// Helper class that holds the current result of the greeks indicators, including the implied volatility, theoretical price and greeks values + /// + public class GreeksIndicatorsResult + { + /// + /// Gets the implied volatility + /// + public decimal ImpliedVolatility { get; init; } + + /// + /// Gets the theoretical price + /// + public decimal TheoreticalPrice { get; init; } + + /// + /// Gets the current greeks values + /// + public Greeks Greeks { get; init; } + + } } diff --git a/Indicators/IndicatorBasedOptionPriceModel.cs b/Indicators/IndicatorBasedOptionPriceModel.cs index 69f9af2fe8ab..933df5fc97b0 100644 --- a/Indicators/IndicatorBasedOptionPriceModel.cs +++ b/Indicators/IndicatorBasedOptionPriceModel.cs @@ -32,6 +32,8 @@ public class IndicatorBasedOptionPriceModel : IOptionPriceModel { private Symbol _contractSymbol; private Symbol _mirrorContractSymbol; + private IDividendYieldModel _dividendYieldModel; + private GreeksIndicators _indicators; /// /// Creates a new containing the theoretical price based on @@ -61,31 +63,35 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon var contractSymbol = _contractSymbol; var mirrorContractSymbol = _mirrorContractSymbol; + var symbolsChanged = false; // These models are supposed to be one per contract (security instance), so we cache the symbols to avoid calling // GetMirrorOptionSymbol multiple times. If the contract changes by any reason, we just update the cached symbols. if (contractSymbol != contract.Symbol) { contractSymbol = _contractSymbol = contract.Symbol; - mirrorContractSymbol = _mirrorContractSymbol = contractSymbol.GetMirrorOptionSymbol(); + mirrorContractSymbol = _mirrorContractSymbol = contractSymbol.GetMirrorOptionSymbol(); + _dividendYieldModel = GreeksIndicators.GetDividendYieldModel(contractSymbol); + symbolsChanged = true; } BaseData underlyingData = null; BaseData optionData = null; BaseData mirrorOptionData = null; - foreach (var underlyingDataType in new[] { typeof(TradeBar), typeof(Tick) }) + foreach (var useBars in new[] { true, false }) { - if (underlyingDataType == typeof(TradeBar)) + if (useBars) { - if (slice.Bars.TryGetValue(contractSymbol.Underlying, out var underlyingTradeBar) && - slice.QuoteBars.TryGetValue(contractSymbol, out var optionQuoteBar) && - underlyingTradeBar.Period == optionQuoteBar.Period) + TradeBar underlyingTradeBar = null; + QuoteBar underlyingQuoteBar = null; + if ((slice.Bars.TryGetValue(contractSymbol.Underlying, out underlyingTradeBar) || + slice.QuoteBars.TryGetValue(contractSymbol.Underlying, out underlyingQuoteBar)) && + slice.QuoteBars.TryGetValue(contractSymbol, out var optionQuoteBar)) { - underlyingData = underlyingTradeBar; + underlyingData = (BaseData)underlyingTradeBar ?? underlyingQuoteBar; optionData = optionQuoteBar; - if (slice.QuoteBars.TryGetValue(mirrorContractSymbol, out var mirrorOptionQuoteBar) && - mirrorOptionQuoteBar.Period == underlyingTradeBar.Period) + if (slice.QuoteBars.TryGetValue(mirrorContractSymbol, out var mirrorOptionQuoteBar)) { mirrorOptionData = mirrorOptionQuoteBar; } @@ -149,28 +155,37 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon mirrorContractSymbol = null; } - var greeksIndicators = new Lazy(() => + var greeksIndicators = new Lazy(() => { - var indicators = new GreeksIndicators(contractSymbol, mirrorContractSymbol); + if (_indicators == null || symbolsChanged || + // The mirror contract can go from null to non-null and vice versa, so we need to check if the symbol has changed in that case as well + (_indicators.UseMirrorOption && mirrorContractSymbol == null) || (!_indicators.UseMirrorOption && mirrorContractSymbol != null)) + { + // We'll try to reuse the indicators instance whenever possible + _indicators = new GreeksIndicators(contractSymbol, mirrorContractSymbol, dividendYieldModel: _dividendYieldModel); + } if (underlyingData != null) { - indicators.Update(underlyingData); + _indicators.Update(underlyingData); } if (optionData != null) { - indicators.Update(optionData); + _indicators.Update(optionData); } if (mirrorOptionData != null) { - indicators.Update(mirrorOptionData); + _indicators.Update(mirrorOptionData); } - return indicators; + var result = _indicators.CurrentResult; + _indicators.Reset(); + + return result; }, isThreadSafe: false); return new OptionPriceModelResult( - () => greeksIndicators.Value.ImpliedVolatility.TheoreticalPrice, + () => greeksIndicators.Value.TheoreticalPrice, () => greeksIndicators.Value.ImpliedVolatility, () => greeksIndicators.Value.Greeks); } From dad21d4f99a7ed2cdea300d96875f4649185f360 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 12 Feb 2026 17:50:40 -0400 Subject: [PATCH 08/18] Support indicators configuration for new pricing model --- .../Data/UniverseSelection/OptionUniverse.cs | 4 +- Indicators/GreeksIndicators.cs | 47 +++--------- Indicators/IndicatorBasedOptionPriceModel.cs | 73 +++++++++++++------ .../IndicatorBasedOptionPriceModelProvider.cs | 18 +++++ 4 files changed, 80 insertions(+), 62 deletions(-) diff --git a/Common/Data/UniverseSelection/OptionUniverse.cs b/Common/Data/UniverseSelection/OptionUniverse.cs index 566af6b1a448..e6e8579722b5 100644 --- a/Common/Data/UniverseSelection/OptionUniverse.cs +++ b/Common/Data/UniverseSelection/OptionUniverse.cs @@ -206,7 +206,7 @@ public static string ToCsv(Symbol symbol, decimal open, decimal high, decimal lo } return $"{GetOptionSymbolCsv(symbol)},{open},{high},{low},{close},{volume}," - + $"{openInterest},{impliedVolatility},{greeks?.Delta},{greeks?.Gamma},{greeks?.Vega},{greeks?.Theta},{greeks?.Rho}"; + + $"{openInterest},{impliedVolatility},{greeks?.Delta},{greeks?.Gamma},{greeks?.Vega},{greeks?.ThetaPerDay},{greeks?.Rho}"; } /// @@ -257,7 +257,7 @@ private class PreCalculatedGreeks : Greeks public override decimal Vega => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex + 2); - public override decimal Theta => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex + 3); + public override decimal ThetaPerDay => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex + 3); public override decimal Rho => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex + 4); diff --git a/Indicators/GreeksIndicators.cs b/Indicators/GreeksIndicators.cs index 5f28add1fbe4..2990e5e5e4e4 100644 --- a/Indicators/GreeksIndicators.cs +++ b/Indicators/GreeksIndicators.cs @@ -72,7 +72,7 @@ public class GreeksIndicators /// /// Gets the current greeks values /// - public Greeks Greeks => new GreeksHolder(Delta, Gamma, Vega, Theta, Rho); + public Greeks Greeks => new Greeks(Delta, Gamma, Vega, Theta * 365m, Rho, 0m); /// /// Whether the mirror option is set and will be used in the calculations. @@ -103,22 +103,21 @@ public static IDividendYieldModel GetDividendYieldModel(Symbol optionSymbol) /// Creates a new instance of the class /// public GreeksIndicators(Symbol optionSymbol, Symbol mirrorOptionSymbol, OptionPricingModelType? optionModel = null, - OptionPricingModelType? ivModel = null, IDividendYieldModel dividendYieldModel = null) + OptionPricingModelType? ivModel = null, IDividendYieldModel dividendYieldModel = null, + IRiskFreeInterestRateModel riskFreeInterestRateModel = null) { _optionSymbol = optionSymbol; _mirrorOptionSymbol = mirrorOptionSymbol; - if (dividendYieldModel == null) - { - dividendYieldModel = GetDividendYieldModel(optionSymbol); - } + dividendYieldModel ??= GetDividendYieldModel(optionSymbol); + riskFreeInterestRateModel ??= _interestRateProvider; - 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); + ImpliedVolatility = new ImpliedVolatility(_optionSymbol, riskFreeInterestRateModel, dividendYieldModel, _mirrorOptionSymbol, ivModel); + Delta = new Delta(_optionSymbol, riskFreeInterestRateModel, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + Gamma = new Gamma(_optionSymbol, riskFreeInterestRateModel, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + Vega = new Vega(_optionSymbol, riskFreeInterestRateModel, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + Theta = new Theta(_optionSymbol, riskFreeInterestRateModel, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); + Rho = new Rho(_optionSymbol, riskFreeInterestRateModel, dividendYieldModel, _mirrorOptionSymbol, optionModel, ivModel); Delta.ImpliedVolatility = ImpliedVolatility; Gamma.ImpliedVolatility = ImpliedVolatility; @@ -152,30 +151,6 @@ public void Reset() Theta.Reset(); Rho.Reset(); } - - 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/IndicatorBasedOptionPriceModel.cs b/Indicators/IndicatorBasedOptionPriceModel.cs index 933df5fc97b0..6e5be65a151e 100644 --- a/Indicators/IndicatorBasedOptionPriceModel.cs +++ b/Indicators/IndicatorBasedOptionPriceModel.cs @@ -16,9 +16,7 @@ using QuantConnect.Data; using QuantConnect.Data.Market; using QuantConnect.Logging; -using QuantConnect.Securities; using QuantConnect.Securities.Option; -using QuantConnect.Util; using System; using System.Linq; @@ -28,29 +26,52 @@ namespace QuantConnect.Indicators /// Provides an implementation of that uses QuantConnect indicators /// to provide a theoretical price for the option contract. /// - public class IndicatorBasedOptionPriceModel : IOptionPriceModel + public class IndicatorBasedOptionPriceModel : OptionPriceModel { private Symbol _contractSymbol; private Symbol _mirrorContractSymbol; + private readonly OptionPricingModelType? _optionPricingModelType; + private readonly OptionPricingModelType? _ivModelType; private IDividendYieldModel _dividendYieldModel; + private readonly IRiskFreeInterestRateModel _riskFreeInterestRateModel; + private readonly bool _userSpecifiedDividendYieldModel; + private readonly bool _useMirrorContract; private GreeksIndicators _indicators; + /// + /// Creates a new instance of the class + /// + /// The option pricing model type to be used by the indicators + /// The option pricing model type to be used by the implied volatility indicator + /// The dividend yield model to be used by the indicators + /// The risk free interest rate model to be used by the indicators + /// Whether to use the mirror contract when possible + public IndicatorBasedOptionPriceModel(OptionPricingModelType? optionModel = null, + OptionPricingModelType? ivModel = null, IDividendYieldModel dividendYieldModel = null, + IRiskFreeInterestRateModel riskFreeInterestRateModel = null, bool useMirrorContract = true) + { + _optionPricingModelType = optionModel; + _ivModelType = ivModel; + _dividendYieldModel = dividendYieldModel; + _riskFreeInterestRateModel = riskFreeInterestRateModel; + _useMirrorContract = useMirrorContract; + _userSpecifiedDividendYieldModel = dividendYieldModel != null; + } + /// /// 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 + /// The evaluation parameters /// /// An instance of containing the theoretical /// price of the specified option contract. /// - public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionContract contract) + public override OptionPriceModelResult Evaluate(OptionPriceModelParameters parameters) { + var contract = parameters.Contract; + var slice = parameters.Slice; + // expired options have no price if (contract.Time.Date > contract.Expiry.Date) { @@ -69,8 +90,17 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon if (contractSymbol != contract.Symbol) { contractSymbol = _contractSymbol = contract.Symbol; - mirrorContractSymbol = _mirrorContractSymbol = contractSymbol.GetMirrorOptionSymbol(); - _dividendYieldModel = GreeksIndicators.GetDividendYieldModel(contractSymbol); + + if (_useMirrorContract) + { + mirrorContractSymbol = _mirrorContractSymbol = contractSymbol.GetMirrorOptionSymbol(); + } + + if (!_userSpecifiedDividendYieldModel) + { + _dividendYieldModel = GreeksIndicators.GetDividendYieldModel(contractSymbol); + } + symbolsChanged = true; } @@ -91,7 +121,7 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon underlyingData = (BaseData)underlyingTradeBar ?? underlyingQuoteBar; optionData = optionQuoteBar; - if (slice.QuoteBars.TryGetValue(mirrorContractSymbol, out var mirrorOptionQuoteBar)) + if (_useMirrorContract && slice.QuoteBars.TryGetValue(mirrorContractSymbol, out var mirrorOptionQuoteBar)) { mirrorOptionData = mirrorOptionQuoteBar; } @@ -105,18 +135,14 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon slice.Ticks.TryGetValue(contractSymbol, out var optionTicks)) { // Get last underlying trade tick - underlyingData = underlyingTicks - .Where(x => x.TickType == TickType.Trade) - .LastOrDefault(); + underlyingData = underlyingTicks.LastOrDefault(x => x.TickType == TickType.Trade); if (underlyingData == null) { continue; } // Get last option quote tick - optionData = optionTicks - .Where(x => x.TickType == TickType.Quote) - .LastOrDefault(); + optionData = optionTicks.LastOrDefault(x => x.TickType == TickType.Quote); if (optionData == null) { underlyingData = null; @@ -124,11 +150,9 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon } // Try to get last mirror option quote tick - if (slice.Ticks.TryGetValue(_mirrorContractSymbol, out var mirrorOptionTicks)) + if (_useMirrorContract && slice.Ticks.TryGetValue(_mirrorContractSymbol, out var mirrorOptionTicks)) { - mirrorOptionData = mirrorOptionTicks - .Where(x => x.TickType == TickType.Quote) - .LastOrDefault(); + mirrorOptionData = mirrorOptionTicks.LastOrDefault(x => x.TickType == TickType.Quote); } break; @@ -162,7 +186,8 @@ public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionCon (_indicators.UseMirrorOption && mirrorContractSymbol == null) || (!_indicators.UseMirrorOption && mirrorContractSymbol != null)) { // We'll try to reuse the indicators instance whenever possible - _indicators = new GreeksIndicators(contractSymbol, mirrorContractSymbol, dividendYieldModel: _dividendYieldModel); + _indicators = new GreeksIndicators(contractSymbol, mirrorContractSymbol, _optionPricingModelType, _ivModelType, + _dividendYieldModel, _riskFreeInterestRateModel); } if (underlyingData != null) diff --git a/Indicators/IndicatorBasedOptionPriceModelProvider.cs b/Indicators/IndicatorBasedOptionPriceModelProvider.cs index 677b7fb5188e..4cbd0968a00c 100644 --- a/Indicators/IndicatorBasedOptionPriceModelProvider.cs +++ b/Indicators/IndicatorBasedOptionPriceModelProvider.cs @@ -13,6 +13,7 @@ * limitations under the License. */ +using QuantConnect.Data; using QuantConnect.Securities.Option; namespace QuantConnect.Indicators @@ -40,5 +41,22 @@ public IOptionPriceModel GetOptionPriceModel(Symbol symbol) { return new IndicatorBasedOptionPriceModel(); } + + /// + /// Gets the option price model with the specified configuration + /// + /// The option pricing model type to be used by the indicators + /// The option pricing model type to be used by the implied volatility indicator + /// The dividend yield model to be used by the indicators + /// The risk free interest rate model to be used by the indicatorsv + /// Whether to use the mirror contract when possible + /// The option price model for the given symbol + public IOptionPriceModel GetOptionPriceModel(OptionPricingModelType? optionModel = null, + OptionPricingModelType? ivModel = null, IDividendYieldModel dividendYieldModel = null, + IRiskFreeInterestRateModel riskFreeInterestRateModel = null, + bool useMirrorContract = true) + { + return new IndicatorBasedOptionPriceModel(optionModel, ivModel, dividendYieldModel, riskFreeInterestRateModel, useMirrorContract); + } } } From 41b29ec2831ef67ef4f5c91c95e7026d3abade0e Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 13 Feb 2026 16:53:26 -0400 Subject: [PATCH 09/18] Some cleanup --- .../Data/UniverseSelection/OptionUniverse.cs | 2 +- .../Option/OptionPriceModelResult.cs | 4 ++ Indicators/GreeksIndicators.cs | 30 +----------- Indicators/IndicatorBasedOptionPriceModel.cs | 46 +++++++------------ 4 files changed, 23 insertions(+), 59 deletions(-) diff --git a/Common/Data/UniverseSelection/OptionUniverse.cs b/Common/Data/UniverseSelection/OptionUniverse.cs index e6e8579722b5..b2c30b7412dd 100644 --- a/Common/Data/UniverseSelection/OptionUniverse.cs +++ b/Common/Data/UniverseSelection/OptionUniverse.cs @@ -257,7 +257,7 @@ private class PreCalculatedGreeks : Greeks public override decimal Vega => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex + 2); - public override decimal ThetaPerDay => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex + 3); + public override decimal Theta => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex + 3) * 365m; public override decimal Rho => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex + 4); diff --git a/Common/Securities/Option/OptionPriceModelResult.cs b/Common/Securities/Option/OptionPriceModelResult.cs index db2708f41571..eb4ecc5f18e5 100644 --- a/Common/Securities/Option/OptionPriceModelResult.cs +++ b/Common/Securities/Option/OptionPriceModelResult.cs @@ -42,6 +42,10 @@ public decimal TheoreticalPrice { return _theoreticalPrice.Value; } + set + { + _theoreticalPrice = new Lazy(() => value, isThreadSafe: false); + } } /// diff --git a/Indicators/GreeksIndicators.cs b/Indicators/GreeksIndicators.cs index 2990e5e5e4e4..93d42a36e069 100644 --- a/Indicators/GreeksIndicators.cs +++ b/Indicators/GreeksIndicators.cs @@ -15,6 +15,7 @@ using QuantConnect.Data; using QuantConnect.Data.Market; +using QuantConnect.Securities.Option; namespace QuantConnect.Indicators { @@ -82,12 +83,7 @@ public class GreeksIndicators /// /// Gets the current result of the greeks indicators, including the implied volatility, theoretical price and greeks values /// - public GreeksIndicatorsResult CurrentResult => new GreeksIndicatorsResult - { - ImpliedVolatility = ImpliedVolatility, - TheoreticalPrice = ImpliedVolatility.TheoreticalPrice, - Greeks = Greeks - }; + public OptionPriceModelResult CurrentResult => new OptionPriceModelResult(ImpliedVolatility.TheoreticalPrice, ImpliedVolatility, Greeks); /// /// Gets the dividend yield model to be used in the calculations for the specified option symbol. @@ -152,26 +148,4 @@ public void Reset() Rho.Reset(); } } - - /// - /// Helper class that holds the current result of the greeks indicators, including the implied volatility, theoretical price and greeks values - /// - public class GreeksIndicatorsResult - { - /// - /// Gets the implied volatility - /// - public decimal ImpliedVolatility { get; init; } - - /// - /// Gets the theoretical price - /// - public decimal TheoreticalPrice { get; init; } - - /// - /// Gets the current greeks values - /// - public Greeks Greeks { get; init; } - - } } diff --git a/Indicators/IndicatorBasedOptionPriceModel.cs b/Indicators/IndicatorBasedOptionPriceModel.cs index 6e5be65a151e..b82957134358 100644 --- a/Indicators/IndicatorBasedOptionPriceModel.cs +++ b/Indicators/IndicatorBasedOptionPriceModel.cs @@ -179,40 +179,26 @@ public override OptionPriceModelResult Evaluate(OptionPriceModelParameters param mirrorContractSymbol = null; } - var greeksIndicators = new Lazy(() => + if (_indicators == null || symbolsChanged || + // The mirror contract can go from null to non-null and vice versa, so we need to check if the symbol has changed in that case as well + (_indicators.UseMirrorOption && mirrorContractSymbol == null) || (!_indicators.UseMirrorOption && mirrorContractSymbol != null)) { - if (_indicators == null || symbolsChanged || - // The mirror contract can go from null to non-null and vice versa, so we need to check if the symbol has changed in that case as well - (_indicators.UseMirrorOption && mirrorContractSymbol == null) || (!_indicators.UseMirrorOption && mirrorContractSymbol != null)) - { - // We'll try to reuse the indicators instance whenever possible - _indicators = new GreeksIndicators(contractSymbol, mirrorContractSymbol, _optionPricingModelType, _ivModelType, - _dividendYieldModel, _riskFreeInterestRateModel); - } - - if (underlyingData != null) - { - _indicators.Update(underlyingData); - } - if (optionData != null) - { - _indicators.Update(optionData); - } - if (mirrorOptionData != null) - { - _indicators.Update(mirrorOptionData); - } + // We'll try to reuse the indicators instance whenever possible + _indicators = new GreeksIndicators(contractSymbol, mirrorContractSymbol, _optionPricingModelType, _ivModelType, + _dividendYieldModel, _riskFreeInterestRateModel); + } - var result = _indicators.CurrentResult; - _indicators.Reset(); + _indicators.Update(underlyingData); + _indicators.Update(optionData); + if (mirrorOptionData != null) + { + _indicators.Update(mirrorOptionData); + } - return result; - }, isThreadSafe: false); + var result = _indicators.CurrentResult; + _indicators.Reset(); - return new OptionPriceModelResult( - () => greeksIndicators.Value.TheoreticalPrice, - () => greeksIndicators.Value.ImpliedVolatility, - () => greeksIndicators.Value.Greeks); + return result; } } } From 2db690de51a439192b0d3683a443c329ef82edfb Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 16 Feb 2026 15:35:05 -0400 Subject: [PATCH 10/18] Add QL option price model example algorithm --- ...edOptionPricingModelRegressionAlgorithm.cs | 3 +- ...QLOptionPricingModelRegressionAlgorithm.cs | 145 ++++++++++++++++++ ...QLOptionPricingModelRegressionAlgorithm.py | 59 +++++++ 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 Algorithm.CSharp/QLOptionPricingModelRegressionAlgorithm.cs create mode 100644 Algorithm.Python/QLOptionPricingModelRegressionAlgorithm.py diff --git a/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs b/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs index 0c19ccf3e6e2..63be502262ad 100644 --- a/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs +++ b/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs @@ -98,7 +98,8 @@ public override void OnData(Slice slice) } // Manually evaluate the price model, just in case - var result = _option.EvaluatePriceModel(slice, contract); + var security = Securities[contract.Symbol] as Option; + var result = security.EvaluatePriceModel(slice, contract); if (result == null || result.TheoreticalPrice != theoreticalPrice || diff --git a/Algorithm.CSharp/QLOptionPricingModelRegressionAlgorithm.cs b/Algorithm.CSharp/QLOptionPricingModelRegressionAlgorithm.cs new file mode 100644 index 000000000000..cb6924cb3588 --- /dev/null +++ b/Algorithm.CSharp/QLOptionPricingModelRegressionAlgorithm.cs @@ -0,0 +1,145 @@ +/* + * 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.Securities.Option; + +namespace QuantConnect.Algorithm.CSharp +{ + /// + /// This example demonstrates how to override the option pricing model with the + /// for a given option security. + /// + public class QLOptionPricingModelRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition + { + private bool _checked; + + private Option _option; + + public override void Initialize() + { + SetStartDate(2015, 12, 24); + SetEndDate(2015, 12, 24); + SetCash(100000); + + var equity = AddEquity("GOOG"); + _option = AddOption(equity.Symbol); + _option.SetFilter(u => u.Strikes(-2, +2).Expiration(0, 180)); + + // Set the option price model to the default QL model + _option.SetPriceModel(QLOptionPriceModelProvider.Instance.GetOptionPriceModel(_option.Symbol)); + + if (_option.PriceModel is not QLOptionPriceModel) + { + throw new RegressionTestException("Option pricing model was not set to QLOptionPriceModel, which should be the default"); + } + } + + public override void OnData(Slice slice) + { + if (!_checked && slice.OptionChains.TryGetValue(_option.Symbol, out var chain)) + { + if (_option.PriceModel is not QLOptionPriceModel) + { + throw new RegressionTestException("Option pricing model was not set to QLOptionPriceModel"); + } + + 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}"); + + _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, Language.Python }; + + /// + /// 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/Algorithm.Python/QLOptionPricingModelRegressionAlgorithm.py b/Algorithm.Python/QLOptionPricingModelRegressionAlgorithm.py new file mode 100644 index 000000000000..6c2854f4d69e --- /dev/null +++ b/Algorithm.Python/QLOptionPricingModelRegressionAlgorithm.py @@ -0,0 +1,59 @@ +# 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. + +from AlgorithmImports import * + +### +### This example demonstrates how to override the option pricing model with the +### for a given option security. +### +class QLOptionPricingModelRegressionAlgorithm(QCAlgorithm): + + def initialize(self): + self.set_start_date(2015, 12, 24) + self.set_end_date(2015, 12, 24) + self.set_cash(100000) + + equity = self.add_equity("GOOG") + self._option = self.add_option(equity.symbol) + self._option.set_filter(lambda u: u.strikes(-2, +2).expiration(0, 180)) + + # Set the option price model to the default QL model + self._option.set_price_model(QLOptionPriceModelProvider.INSTANCE.get_option_price_model(self._option.symbol)) + + if not isinstance(self._option.price_model, QLOptionPriceModel): + raise Exception("Option pricing model was not set to QLOptionPriceModel, which should be the default") + + self._checked = False + + def on_data(self, slice): + if self._checked: + return; + + chain = slice.option_chains.get(self._option.symbol) + if chain is not None: + if not isinstance(self._option.price_model, QLOptionPriceModel): + raise Exception("Option pricing model was not set to QLOptionPriceModel"); + + for contract in chain: + theoretical_price = contract.theoretical_price + iv = contract.implied_volatility + greeks = contract.greeks + self.log(f"{contract.symbol}:: Theoretical Price: {theoretical_price}, IV: {iv}, " + + f"Delta: {greeks.delta}, Gamma: {greeks.gamma}, Vega: {greeks.vega}, " + + f"Theta: {greeks.theta}, Rho: {greeks.rho}, Lambda: {greeks.lambda_}") + self._checked = True + + def on_end_of_algorithm(self): + if not self._checked: + raise Exception("Option chain was never received.") From d94c9a0e636a53260ea489aa2150cbcf8dc84c0b Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 17 Feb 2026 10:07:39 -0400 Subject: [PATCH 11/18] Return lean models from static helpers --- .../Option/IOptionPriceModelProvider.cs | 12 ++++++++ Common/Securities/Option/OptionPriceModels.cs | 29 ++++++++++--------- .../Option/QLOptionPriceModelProvider.cs | 24 +++++++++++++++ Common/Securities/SecurityService.cs | 1 + .../IndicatorBasedOptionPriceModelProvider.cs | 18 ++++++++++++ 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/Common/Securities/Option/IOptionPriceModelProvider.cs b/Common/Securities/Option/IOptionPriceModelProvider.cs index a13ebfc9ebde..3c8abb6d6f6e 100644 --- a/Common/Securities/Option/IOptionPriceModelProvider.cs +++ b/Common/Securities/Option/IOptionPriceModelProvider.cs @@ -26,5 +26,17 @@ public interface IOptionPriceModelProvider /// The symbol /// The option price model for the given symbol IOptionPriceModel GetOptionPriceModel(Symbol symbol); + + /// + /// Gets the default option price model for the specified option symbol using Black-Scholes model + /// + /// The option price model + IOptionPriceModel BlackScholes(); + + /// + /// Gets the default option price model for the specified option symbol using Binomial Cox-Ross-Rubinstein (CRR) model + /// + /// The option price model + IOptionPriceModel BinomialCoxRossRubinstein(); } } diff --git a/Common/Securities/Option/OptionPriceModels.cs b/Common/Securities/Option/OptionPriceModels.cs index 6132ccdcbcdd..9cd3cb02cfb9 100644 --- a/Common/Securities/Option/OptionPriceModels.cs +++ b/Common/Securities/Option/OptionPriceModels.cs @@ -33,9 +33,13 @@ namespace QuantConnect.Securities.Option /// public static class OptionPriceModels { - private const int _timeStepsBinomial = 100; private const int _timeStepsFD = 100; + /// + /// Default option price model provider used by LEAN when creating price models. + /// + internal static IOptionPriceModelProvider DefaultPriceModelProvider { get; set; } + /// /// Creates pricing engine by engine type name. /// @@ -57,14 +61,12 @@ public static IOptionPriceModel Create(string priceEngineName, decimal riskFree, } /// - /// Pricing engine for European vanilla options using analytical formula. - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_analytic_european_engine.html + /// Pricing engine for Black-Scholes model. /// /// New option price model instance public static IOptionPriceModel BlackScholes() { - return new QLOptionPriceModel(process => new AnalyticEuropeanEngine(process), - allowedOptionStyles: new[] { OptionStyle.European }); + return DefaultPriceModelProvider.BlackScholes(); } /// @@ -122,18 +124,17 @@ public static IOptionPriceModel CrankNicolsonFD() /// New option price model instance public static IOptionPriceModel BinomialJarrowRudd() { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, _timeStepsBinomial)); + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); } /// - /// Pricing engine for European and American vanilla options using binomial trees. Cox-Ross-Rubinstein(CRR) model. - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html + /// Pricing engine for Cox-Ross-Rubinstein (CRR) model. /// /// New option price model instance public static IOptionPriceModel BinomialCoxRossRubinstein() { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, _timeStepsBinomial)); + return DefaultPriceModelProvider.BinomialCoxRossRubinstein(); } /// @@ -143,7 +144,7 @@ public static IOptionPriceModel BinomialCoxRossRubinstein() /// New option price model instance public static IOptionPriceModel AdditiveEquiprobabilities() { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, _timeStepsBinomial)); + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); } /// @@ -153,7 +154,7 @@ public static IOptionPriceModel AdditiveEquiprobabilities() /// New option price model instance public static IOptionPriceModel BinomialTrigeorgis() { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, _timeStepsBinomial)); + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); } /// @@ -163,7 +164,7 @@ public static IOptionPriceModel BinomialTrigeorgis() /// New option price model instance public static IOptionPriceModel BinomialTian() { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, _timeStepsBinomial)); + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); } /// @@ -173,7 +174,7 @@ public static IOptionPriceModel BinomialTian() /// New option price model instance public static IOptionPriceModel BinomialLeisenReimer() { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, _timeStepsBinomial)); + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); } /// @@ -183,7 +184,7 @@ public static IOptionPriceModel BinomialLeisenReimer() /// New option price model instance public static IOptionPriceModel BinomialJoshi() { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, _timeStepsBinomial)); + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); } } diff --git a/Common/Securities/Option/QLOptionPriceModelProvider.cs b/Common/Securities/Option/QLOptionPriceModelProvider.cs index b43444137b9d..b09b663fee51 100644 --- a/Common/Securities/Option/QLOptionPriceModelProvider.cs +++ b/Common/Securities/Option/QLOptionPriceModelProvider.cs @@ -13,6 +13,7 @@ * limitations under the License. */ +using QLNet; using System; namespace QuantConnect.Securities.Option @@ -22,6 +23,8 @@ namespace QuantConnect.Securities.Option /// public class QLOptionPriceModelProvider : IOptionPriceModelProvider { + internal const int TimeStepsBinomial = 100; + /// /// Singleton instance of the /// @@ -51,5 +54,26 @@ public IOptionPriceModel GetOptionPriceModel(Symbol symbol) _ => throw new ArgumentException("Invalid OptionStyle") }; } + + /// + /// Pricing engine for European vanilla options using analytical formula. + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_analytic_european_engine.html + /// + /// New option price model instance + public IOptionPriceModel BlackScholes() + { + return new QLOptionPriceModel(process => new AnalyticEuropeanEngine(process), + allowedOptionStyles: [OptionStyle.European]); + } + + /// + /// Pricing engine for European and American vanilla options using binomial trees. Cox-Ross-Rubinstein(CRR) model. + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html + /// + /// New option price model instance + public IOptionPriceModel BinomialCoxRossRubinstein() + { + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, TimeStepsBinomial)); + } } } diff --git a/Common/Securities/SecurityService.cs b/Common/Securities/SecurityService.cs index b0ad767881e4..c6d820f895f5 100644 --- a/Common/Securities/SecurityService.cs +++ b/Common/Securities/SecurityService.cs @@ -64,6 +64,7 @@ public SecurityService(CashBook cashBook, _primaryExchangeProvider = primaryExchangeProvider; _algorithm = algorithm; _optionPriceModelProvider = optionPriceModelProvider; + OptionPriceModels.DefaultPriceModelProvider = _optionPriceModelProvider; } /// diff --git a/Indicators/IndicatorBasedOptionPriceModelProvider.cs b/Indicators/IndicatorBasedOptionPriceModelProvider.cs index 4cbd0968a00c..9a04d0768d29 100644 --- a/Indicators/IndicatorBasedOptionPriceModelProvider.cs +++ b/Indicators/IndicatorBasedOptionPriceModelProvider.cs @@ -42,6 +42,24 @@ public IOptionPriceModel GetOptionPriceModel(Symbol symbol) return new IndicatorBasedOptionPriceModel(); } + /// + /// Gets the default option price model for the specified option symbol using Black-Scholes model + /// + /// The option price model + public IOptionPriceModel BlackScholes() + { + return GetOptionPriceModel(OptionPricingModelType.BlackScholes, OptionPricingModelType.BlackScholes); + } + + /// + /// Gets the default option price model for the specified option symbol using Binomial Cox-Ross-Rubinstein (CRR) model + /// + /// The option price model + public IOptionPriceModel BinomialCoxRossRubinstein() + { + return GetOptionPriceModel(OptionPricingModelType.BinomialCoxRossRubinstein, OptionPricingModelType.BinomialCoxRossRubinstein); + } + /// /// Gets the option price model with the specified configuration /// From a751838bbbde98a65febe5410cfb13e4c51a0ef3 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 17 Feb 2026 17:31:31 -0400 Subject: [PATCH 12/18] Minor tests fixes --- .../IndexOptionCallITMGreeksExpiryRegressionAlgorithm.cs | 5 ----- ...ceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs | 4 ++-- .../OptionUniverseFilterGreeksRegressionAlgorithm.cs | 4 ++-- .../IndexOptionCallITMGreeksExpiryRegressionAlgorithm.py | 4 ---- ...ceModelForUnsupportedAmericanOptionRegressionAlgorithm.py | 2 +- .../OptionUniverseFilterGreeksRegressionAlgorithm.py | 4 ++-- Common/Securities/Option/OptionPriceModels.cs | 2 +- Common/Securities/Option/QLOptionPriceModelProvider.cs | 4 ++-- 8 files changed, 10 insertions(+), 19 deletions(-) diff --git a/Algorithm.CSharp/IndexOptionCallITMGreeksExpiryRegressionAlgorithm.cs b/Algorithm.CSharp/IndexOptionCallITMGreeksExpiryRegressionAlgorithm.cs index 77a743213691..680cc1108da2 100644 --- a/Algorithm.CSharp/IndexOptionCallITMGreeksExpiryRegressionAlgorithm.cs +++ b/Algorithm.CSharp/IndexOptionCallITMGreeksExpiryRegressionAlgorithm.cs @@ -84,7 +84,6 @@ public override void OnData(Slice slice) var deltas = slice.OptionChains.Values.OrderByDescending(y => y.Contracts.Values.Sum(x => x.Volume)).First().Contracts.Values.Select(x => x.Greeks.Delta).ToList(); var gammas = slice.OptionChains.Values.OrderByDescending(y => y.Contracts.Values.Sum(x => x.Volume)).First().Contracts.Values.Select(x => x.Greeks.Gamma).ToList(); - var lambda = slice.OptionChains.Values.OrderByDescending(y => y.Contracts.Values.Sum(x => x.Volume)).First().Contracts.Values.Select(x => x.Greeks.Lambda).ToList(); var rho = slice.OptionChains.Values.OrderByDescending(y => y.Contracts.Values.Sum(x => x.Volume)).First().Contracts.Values.Select(x => x.Greeks.Rho).ToList(); var theta = slice.OptionChains.Values.OrderByDescending(y => y.Contracts.Values.Sum(x => x.Volume)).First().Contracts.Values.Select(x => x.Greeks.Theta).ToList(); var impliedVol = slice.OptionChains.Values.OrderByDescending(y => y.Contracts.Values.Sum(x => x.Volume)).First().Contracts.Values.Select(x => x.ImpliedVolatility).ToList(); @@ -102,10 +101,6 @@ public override void OnData(Slice slice) { throw new AggregateException("Option contract Gamma was equal to zero"); } - if (lambda.Any(l => l == 0)) - { - throw new AggregateException("Option contract Lambda was equal to zero"); - } if (rho.Any(r => r == 0)) { throw new AggregateException("Option contract Rho was equal to zero"); diff --git a/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs b/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs index ca1764623e9c..146e3f942b39 100644 --- a/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs @@ -31,8 +31,8 @@ public override void Initialize() var option = AddOption("AAPL", Resolution.Minute); option.SetFilter(u => u.StandardsOnly().Strikes(-1, 1).Expiration(0, 35)); - // BlackSholes model does not support American style options - option.PriceModel = OptionPriceModels.BlackScholes(); + // QL BlackSholes model does not support American style options + option.PriceModel = QLOptionPriceModelProvider.Instance.BlackScholes(); SetWarmup(2, Resolution.Daily); diff --git a/Algorithm.CSharp/OptionUniverseFilterGreeksRegressionAlgorithm.cs b/Algorithm.CSharp/OptionUniverseFilterGreeksRegressionAlgorithm.cs index 269db4120b82..b328b16a21de 100644 --- a/Algorithm.CSharp/OptionUniverseFilterGreeksRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionUniverseFilterGreeksRegressionAlgorithm.cs @@ -62,8 +62,8 @@ public override void Initialize() MaxGamma = 0.0006m; MinVega = 0.01m; MaxVega = 1.5m; - MinTheta = -2.0m; - MaxTheta = -0.5m; + MinTheta = -730m; + MaxTheta = -182.5m; MinRho = 0.5m; MaxRho = 3.0m; MinIv = 1.0m; diff --git a/Algorithm.Python/IndexOptionCallITMGreeksExpiryRegressionAlgorithm.py b/Algorithm.Python/IndexOptionCallITMGreeksExpiryRegressionAlgorithm.py index c4d51c689dd9..c6e03c478896 100644 --- a/Algorithm.Python/IndexOptionCallITMGreeksExpiryRegressionAlgorithm.py +++ b/Algorithm.Python/IndexOptionCallITMGreeksExpiryRegressionAlgorithm.py @@ -61,7 +61,6 @@ def on_data(self, data: Slice): deltas = [i.greeks.delta for i in self.sort_by_max_volume(data)] gammas = [i.greeks.gamma for i in self.sort_by_max_volume(data)] #data.option_chains.values().order_by_descending(y => y.contracts.values().sum(x => x.volume)).first().contracts.values().select(x => x.greeks.gamma).to_list() - lambda_ = [i.greeks.lambda_ for i in self.sort_by_max_volume(data)] #data.option_chains.values().order_by_descending(y => y.contracts.values().sum(x => x.volume)).first().contracts.values().select(x => x.greeks.lambda).to_list() rho = [i.greeks.rho for i in self.sort_by_max_volume(data)] #data.option_chains.values().order_by_descending(y => y.contracts.values().sum(x => x.volume)).first().contracts.values().select(x => x.greeks.rho).to_list() theta = [i.greeks.theta for i in self.sort_by_max_volume(data)] #data.option_chains.values().order_by_descending(y => y.contracts.values().sum(x => x.volume)).first().contracts.values().select(x => x.greeks.theta).to_list() vega = [i.greeks.vega for i in self.sort_by_max_volume(data)] #data.option_chains.values().order_by_descending(y => y.contracts.values().sum(x => x.volume)).first().contracts.values().select(x => x.greeks.vega).to_list() @@ -76,9 +75,6 @@ def on_data(self, data: Slice): if any([i for i in gammas if i == 0]): raise AssertionError("Option contract Gamma was equal to zero") - if any([i for i in lambda_ if lambda_ == 0]): - raise AssertionError("Option contract Lambda was equal to zero") - if any([i for i in rho if i == 0]): raise AssertionError("Option contract Rho was equal to zero") diff --git a/Algorithm.Python/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.py b/Algorithm.Python/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.py index 072af5b2496b..22f8a6ad477a 100644 --- a/Algorithm.Python/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.py +++ b/Algorithm.Python/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.py @@ -27,7 +27,7 @@ def initialize(self): option.set_filter(lambda u: u.standards_only().strikes(-1, 1).expiration(0, 35)) # BlackSholes model does not support American style options - option.price_model = OptionPriceModels.black_scholes() + option.price_model = QLOptionPriceModelProvider.INSTANCE.black_scholes() self.set_warmup(2, Resolution.DAILY) diff --git a/Algorithm.Python/OptionUniverseFilterGreeksRegressionAlgorithm.py b/Algorithm.Python/OptionUniverseFilterGreeksRegressionAlgorithm.py index a70ba54f355c..b48f7138b339 100644 --- a/Algorithm.Python/OptionUniverseFilterGreeksRegressionAlgorithm.py +++ b/Algorithm.Python/OptionUniverseFilterGreeksRegressionAlgorithm.py @@ -34,8 +34,8 @@ def initialize(self): self._max_gamma = 0.0006 self._min_vega = 0.01 self._max_vega = 1.5 - self._min_theta = -2.0 - self._max_theta = -0.5 + self._min_theta = -730.0 + self._max_theta = -182.5 self._min_rho = 0.5 self._max_rho = 3.0 self._min_iv = 1.0 diff --git a/Common/Securities/Option/OptionPriceModels.cs b/Common/Securities/Option/OptionPriceModels.cs index 9cd3cb02cfb9..5c3312e6772b 100644 --- a/Common/Securities/Option/OptionPriceModels.cs +++ b/Common/Securities/Option/OptionPriceModels.cs @@ -38,7 +38,7 @@ public static class OptionPriceModels /// /// Default option price model provider used by LEAN when creating price models. /// - internal static IOptionPriceModelProvider DefaultPriceModelProvider { get; set; } + internal static IOptionPriceModelProvider DefaultPriceModelProvider { get; set; } = QLOptionPriceModelProvider.Instance; /// /// Creates pricing engine by engine type name. diff --git a/Common/Securities/Option/QLOptionPriceModelProvider.cs b/Common/Securities/Option/QLOptionPriceModelProvider.cs index b09b663fee51..08ed2c4f9e92 100644 --- a/Common/Securities/Option/QLOptionPriceModelProvider.cs +++ b/Common/Securities/Option/QLOptionPriceModelProvider.cs @@ -49,8 +49,8 @@ public IOptionPriceModel GetOptionPriceModel(Symbol symbol) // Available via: https://downloads.dxfeed.com/specifications/dxLibOptions/Numerical-Methods-versus-Bjerksund-and-Stensland-Approximations-for-American-Options-Pricing-.pdf // Also refer to OptionPriceModelTests.MatchesIBGreeksBulk() test, // we select the most accurate and computational efficient model - OptionStyle.American => OptionPriceModels.BinomialCoxRossRubinstein(), - OptionStyle.European => OptionPriceModels.BlackScholes(), + OptionStyle.American => BinomialCoxRossRubinstein(), + OptionStyle.European => BlackScholes(), _ => throw new ArgumentException("Invalid OptionStyle") }; } From 3afe2ed39267e8a58a4ca8d9964e8c426113ba11 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 17 Feb 2026 18:08:34 -0400 Subject: [PATCH 13/18] Minor test fixes --- .../Common/Securities/Options/OptionFilterUniverseTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/Common/Securities/Options/OptionFilterUniverseTests.cs b/Tests/Common/Securities/Options/OptionFilterUniverseTests.cs index 0803e2f6157b..fa2eaebeb0dd 100644 --- a/Tests/Common/Securities/Options/OptionFilterUniverseTests.cs +++ b/Tests/Common/Securities/Options/OptionFilterUniverseTests.cs @@ -141,7 +141,7 @@ public void FiltersContractsByOpenInterest() [TestCase("Delta", 0.63, 0.64, 4)] [TestCase("Gamma", 0.0008, 0.0011, 4)] [TestCase("Vega", 7.5, 11.3, 5)] - [TestCase("Theta", -1.10, -0.50, 8)] + [TestCase("Theta", -401.50, -182.50, 8)] [TestCase("Rho", 4, 10, 10)] public void FiltersContractsByIndividualGreek(string greekName, decimal greekMinValue, decimal greekMaxValue, int expectedContracts) { @@ -170,8 +170,8 @@ public void FiltersContractsByMultipleGreeks() var deltaMax = 0.68m; var gammaMin = 0.00024m; var gammaMax = 0.0028m; - var thetaMin = -1.40m; - var thetaMax = -0.40m; + var thetaMin = -511m; + var thetaMax = -146m; var expectedContracts = 11; // Set up From abbbe2662f3751192562b3bbc6b1fb9e8ce27cf5 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 18 Feb 2026 10:40:15 -0400 Subject: [PATCH 14/18] Address peer review --- ...edOptionPricingModelRegressionAlgorithm.cs | 11 ++++--- ...portedAmericanOptionRegressionAlgorithm.cs | 3 +- .../Indicators}/OptionPricingModelType.cs | 0 .../Option/IOptionPriceModelProvider.cs | 12 +++---- Common/Securities/Option/OptionPriceModels.cs | 4 +-- .../Option/QLOptionPriceModelProvider.cs | 31 ++++++++----------- .../IndicatorBasedOptionPriceModelProvider.cs | 15 ++------- .../Securities/OptionPriceModelTests.cs | 5 +++ 8 files changed, 35 insertions(+), 46 deletions(-) rename {Indicators => Common/Indicators}/OptionPricingModelType.cs (100%) diff --git a/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs b/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs index 63be502262ad..14b32fc53f17 100644 --- a/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs +++ b/Algorithm.CSharp/IndicatorBasedOptionPricingModelRegressionAlgorithm.cs @@ -63,13 +63,14 @@ 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 contractSecurity = Securities[contract.Symbol] as Option; + if (contractSecurity.PriceModel is not IndicatorBasedOptionPriceModel) + { + throw new RegressionTestException("Contract security pricing model was not set to IndicatorBasedOptionPriceModel"); + } + var theoreticalPrice = contract.TheoreticalPrice; var iv = contract.ImpliedVolatility; var greeks = contract.Greeks; diff --git a/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs b/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs index 146e3f942b39..2a50d4563039 100644 --- a/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs @@ -14,6 +14,7 @@ */ using System.Collections.Generic; +using QuantConnect.Indicators; using QuantConnect.Securities.Option; namespace QuantConnect.Algorithm.CSharp @@ -32,7 +33,7 @@ public override void Initialize() option.SetFilter(u => u.StandardsOnly().Strikes(-1, 1).Expiration(0, 35)); // QL BlackSholes model does not support American style options - option.PriceModel = QLOptionPriceModelProvider.Instance.BlackScholes(); + option.PriceModel = QLOptionPriceModelProvider.Instance.GetOptionPriceModel(OptionPricingModelType.BlackScholes); SetWarmup(2, Resolution.Daily); diff --git a/Indicators/OptionPricingModelType.cs b/Common/Indicators/OptionPricingModelType.cs similarity index 100% rename from Indicators/OptionPricingModelType.cs rename to Common/Indicators/OptionPricingModelType.cs diff --git a/Common/Securities/Option/IOptionPriceModelProvider.cs b/Common/Securities/Option/IOptionPriceModelProvider.cs index 3c8abb6d6f6e..33702b5f3ec6 100644 --- a/Common/Securities/Option/IOptionPriceModelProvider.cs +++ b/Common/Securities/Option/IOptionPriceModelProvider.cs @@ -13,6 +13,8 @@ * limitations under the License. */ +using QuantConnect.Indicators; + namespace QuantConnect.Securities.Option { /// @@ -28,15 +30,9 @@ public interface IOptionPriceModelProvider IOptionPriceModel GetOptionPriceModel(Symbol symbol); /// - /// Gets the default option price model for the specified option symbol using Black-Scholes model - /// - /// The option price model - IOptionPriceModel BlackScholes(); - - /// - /// Gets the default option price model for the specified option symbol using Binomial Cox-Ross-Rubinstein (CRR) model + /// Gets an option price model using the specified option pricing model type /// /// The option price model - IOptionPriceModel BinomialCoxRossRubinstein(); + IOptionPriceModel GetOptionPriceModel(OptionPricingModelType pricingModelType); } } diff --git a/Common/Securities/Option/OptionPriceModels.cs b/Common/Securities/Option/OptionPriceModels.cs index 5c3312e6772b..80b2a66dc05f 100644 --- a/Common/Securities/Option/OptionPriceModels.cs +++ b/Common/Securities/Option/OptionPriceModels.cs @@ -66,7 +66,7 @@ public static IOptionPriceModel Create(string priceEngineName, decimal riskFree, /// New option price model instance public static IOptionPriceModel BlackScholes() { - return DefaultPriceModelProvider.BlackScholes(); + return DefaultPriceModelProvider.GetOptionPriceModel(Indicators.OptionPricingModelType.BlackScholes); } /// @@ -134,7 +134,7 @@ public static IOptionPriceModel BinomialJarrowRudd() /// New option price model instance public static IOptionPriceModel BinomialCoxRossRubinstein() { - return DefaultPriceModelProvider.BinomialCoxRossRubinstein(); + return DefaultPriceModelProvider.GetOptionPriceModel(Indicators.OptionPricingModelType.BinomialCoxRossRubinstein); } /// diff --git a/Common/Securities/Option/QLOptionPriceModelProvider.cs b/Common/Securities/Option/QLOptionPriceModelProvider.cs index 08ed2c4f9e92..b6a76c4690dc 100644 --- a/Common/Securities/Option/QLOptionPriceModelProvider.cs +++ b/Common/Securities/Option/QLOptionPriceModelProvider.cs @@ -14,6 +14,7 @@ */ using QLNet; +using QuantConnect.Indicators; using System; namespace QuantConnect.Securities.Option @@ -49,31 +50,25 @@ public IOptionPriceModel GetOptionPriceModel(Symbol symbol) // Available via: https://downloads.dxfeed.com/specifications/dxLibOptions/Numerical-Methods-versus-Bjerksund-and-Stensland-Approximations-for-American-Options-Pricing-.pdf // Also refer to OptionPriceModelTests.MatchesIBGreeksBulk() test, // we select the most accurate and computational efficient model - OptionStyle.American => BinomialCoxRossRubinstein(), - OptionStyle.European => BlackScholes(), + OptionStyle.American => GetOptionPriceModel(OptionPricingModelType.BinomialCoxRossRubinstein), + OptionStyle.European => GetOptionPriceModel(OptionPricingModelType.BlackScholes), _ => throw new ArgumentException("Invalid OptionStyle") }; } /// - /// Pricing engine for European vanilla options using analytical formula. - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_analytic_european_engine.html + /// Gets an option price model using the specified option pricing model type /// - /// New option price model instance - public IOptionPriceModel BlackScholes() + /// The option price model + public IOptionPriceModel GetOptionPriceModel(OptionPricingModelType pricingModelType) { - return new QLOptionPriceModel(process => new AnalyticEuropeanEngine(process), - allowedOptionStyles: [OptionStyle.European]); - } - - /// - /// Pricing engine for European and American vanilla options using binomial trees. Cox-Ross-Rubinstein(CRR) model. - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html - /// - /// New option price model instance - public IOptionPriceModel BinomialCoxRossRubinstein() - { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, TimeStepsBinomial)); + return pricingModelType switch + { + OptionPricingModelType.BlackScholes => new QLOptionPriceModel(process => new AnalyticEuropeanEngine(process), + allowedOptionStyles: [OptionStyle.European]), + OptionPricingModelType.BinomialCoxRossRubinstein => new QLOptionPriceModel(process => new BinomialVanillaEngine(process, TimeStepsBinomial)), + _ => throw new ArgumentException($"Unsupported pricing model type: {pricingModelType}") + }; } } } diff --git a/Indicators/IndicatorBasedOptionPriceModelProvider.cs b/Indicators/IndicatorBasedOptionPriceModelProvider.cs index 9a04d0768d29..959835e1a046 100644 --- a/Indicators/IndicatorBasedOptionPriceModelProvider.cs +++ b/Indicators/IndicatorBasedOptionPriceModelProvider.cs @@ -43,21 +43,12 @@ public IOptionPriceModel GetOptionPriceModel(Symbol symbol) } /// - /// Gets the default option price model for the specified option symbol using Black-Scholes model + /// Gets an option price model using the specified option pricing model type /// /// The option price model - public IOptionPriceModel BlackScholes() + public IOptionPriceModel GetOptionPriceModel(OptionPricingModelType pricingModelType) { - return GetOptionPriceModel(OptionPricingModelType.BlackScholes, OptionPricingModelType.BlackScholes); - } - - /// - /// Gets the default option price model for the specified option symbol using Binomial Cox-Ross-Rubinstein (CRR) model - /// - /// The option price model - public IOptionPriceModel BinomialCoxRossRubinstein() - { - return GetOptionPriceModel(OptionPricingModelType.BinomialCoxRossRubinstein, OptionPricingModelType.BinomialCoxRossRubinstein); + return GetOptionPriceModel(pricingModelType, pricingModelType); } /// diff --git a/Tests/Common/Securities/OptionPriceModelTests.cs b/Tests/Common/Securities/OptionPriceModelTests.cs index b741bed921b0..3a0abaaa8656 100644 --- a/Tests/Common/Securities/OptionPriceModelTests.cs +++ b/Tests/Common/Securities/OptionPriceModelTests.cs @@ -36,6 +36,11 @@ namespace QuantConnect.Tests.Common [TestFixture] public class OptionPriceModelTests { + [SetUp] + public void SetUp() + { + OptionPriceModels.DefaultPriceModelProvider = QLOptionPriceModelProvider.Instance; + } [Test] public void PutCallParityTest() From 459ad5541c31a494a22dbafc821e8073834f2fea Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 18 Feb 2026 11:07:36 -0400 Subject: [PATCH 15/18] Cleanup --- ...supportedAmericanOptionRegressionAlgorithm.cs | 2 +- ...supportedAmericanOptionRegressionAlgorithm.py | 2 +- .../Option/IOptionPriceModelProvider.cs | 12 ++++-------- Common/Securities/Option/OptionPriceModels.cs | 4 ++-- .../Option/QLOptionPriceModelProvider.cs | 16 +++++++++------- .../IndicatorBasedOptionPriceModelProvider.cs | 14 +++----------- 6 files changed, 20 insertions(+), 30 deletions(-) diff --git a/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs b/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs index 2a50d4563039..49537220213f 100644 --- a/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs @@ -33,7 +33,7 @@ public override void Initialize() option.SetFilter(u => u.StandardsOnly().Strikes(-1, 1).Expiration(0, 35)); // QL BlackSholes model does not support American style options - option.PriceModel = QLOptionPriceModelProvider.Instance.GetOptionPriceModel(OptionPricingModelType.BlackScholes); + option.PriceModel = QLOptionPriceModelProvider.Instance.GetOptionPriceModel(option.Symbol, OptionPricingModelType.BlackScholes); SetWarmup(2, Resolution.Daily); diff --git a/Algorithm.Python/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.py b/Algorithm.Python/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.py index 22f8a6ad477a..ca785c7d110a 100644 --- a/Algorithm.Python/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.py +++ b/Algorithm.Python/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.py @@ -27,7 +27,7 @@ def initialize(self): option.set_filter(lambda u: u.standards_only().strikes(-1, 1).expiration(0, 35)) # BlackSholes model does not support American style options - option.price_model = QLOptionPriceModelProvider.INSTANCE.black_scholes() + option.price_model = QLOptionPriceModelProvider.INSTANCE.get_option_price_model(option.symbol, OptionPricingModelType.BLACK_SCHOLES) self.set_warmup(2, Resolution.DAILY) diff --git a/Common/Securities/Option/IOptionPriceModelProvider.cs b/Common/Securities/Option/IOptionPriceModelProvider.cs index 33702b5f3ec6..bc0b016503df 100644 --- a/Common/Securities/Option/IOptionPriceModelProvider.cs +++ b/Common/Securities/Option/IOptionPriceModelProvider.cs @@ -23,16 +23,12 @@ namespace QuantConnect.Securities.Option public interface IOptionPriceModelProvider { /// - /// Gets the option price model for the specified option symbol + /// Gets the option price model for the specified option symbol. + /// If no pricing model is specified, the default option price model for the symbol security type will be returned. /// /// The symbol + /// The option pricing model type to use /// The option price model for the given symbol - IOptionPriceModel GetOptionPriceModel(Symbol symbol); - - /// - /// Gets an option price model using the specified option pricing model type - /// - /// The option price model - IOptionPriceModel GetOptionPriceModel(OptionPricingModelType pricingModelType); + IOptionPriceModel GetOptionPriceModel(Symbol symbol, OptionPricingModelType? pricingModelType = null); } } diff --git a/Common/Securities/Option/OptionPriceModels.cs b/Common/Securities/Option/OptionPriceModels.cs index 80b2a66dc05f..ba23f3f3f44f 100644 --- a/Common/Securities/Option/OptionPriceModels.cs +++ b/Common/Securities/Option/OptionPriceModels.cs @@ -66,7 +66,7 @@ public static IOptionPriceModel Create(string priceEngineName, decimal riskFree, /// New option price model instance public static IOptionPriceModel BlackScholes() { - return DefaultPriceModelProvider.GetOptionPriceModel(Indicators.OptionPricingModelType.BlackScholes); + return DefaultPriceModelProvider.GetOptionPriceModel(Symbol.Empty, Indicators.OptionPricingModelType.BlackScholes); } /// @@ -134,7 +134,7 @@ public static IOptionPriceModel BinomialJarrowRudd() /// New option price model instance public static IOptionPriceModel BinomialCoxRossRubinstein() { - return DefaultPriceModelProvider.GetOptionPriceModel(Indicators.OptionPricingModelType.BinomialCoxRossRubinstein); + return DefaultPriceModelProvider.GetOptionPriceModel(Symbol.Empty, Indicators.OptionPricingModelType.BinomialCoxRossRubinstein); } /// diff --git a/Common/Securities/Option/QLOptionPriceModelProvider.cs b/Common/Securities/Option/QLOptionPriceModelProvider.cs index b6a76c4690dc..0ae53a331e4a 100644 --- a/Common/Securities/Option/QLOptionPriceModelProvider.cs +++ b/Common/Securities/Option/QLOptionPriceModelProvider.cs @@ -39,9 +39,15 @@ private QLOptionPriceModelProvider() /// Gets the option price model for the specified option symbol /// /// The symbol + /// The option pricing model type to use /// The option price model for the given symbol - public IOptionPriceModel GetOptionPriceModel(Symbol symbol) + public IOptionPriceModel GetOptionPriceModel(Symbol symbol, OptionPricingModelType? pricingModelType = null) { + if (pricingModelType.HasValue) + { + return GetOptionPriceModel(pricingModelType.Value); + } + return symbol.ID.OptionStyle switch { // CRR model has the best accuracy and speed suggested by @@ -55,12 +61,8 @@ public IOptionPriceModel GetOptionPriceModel(Symbol symbol) _ => throw new ArgumentException("Invalid OptionStyle") }; } - - /// - /// Gets an option price model using the specified option pricing model type - /// - /// The option price model - public IOptionPriceModel GetOptionPriceModel(OptionPricingModelType pricingModelType) + + private static QLOptionPriceModel GetOptionPriceModel(OptionPricingModelType pricingModelType) { return pricingModelType switch { diff --git a/Indicators/IndicatorBasedOptionPriceModelProvider.cs b/Indicators/IndicatorBasedOptionPriceModelProvider.cs index 959835e1a046..3e8186109e18 100644 --- a/Indicators/IndicatorBasedOptionPriceModelProvider.cs +++ b/Indicators/IndicatorBasedOptionPriceModelProvider.cs @@ -36,19 +36,11 @@ private IndicatorBasedOptionPriceModelProvider() /// Gets the option price model for the specified option symbol /// /// The symbol + /// The option pricing model type to use /// The option price model for the given symbol - public IOptionPriceModel GetOptionPriceModel(Symbol symbol) + public IOptionPriceModel GetOptionPriceModel(Symbol symbol, OptionPricingModelType? pricingModelType = null) { - return new IndicatorBasedOptionPriceModel(); - } - - /// - /// Gets an option price model using the specified option pricing model type - /// - /// The option price model - public IOptionPriceModel GetOptionPriceModel(OptionPricingModelType pricingModelType) - { - return GetOptionPriceModel(pricingModelType, pricingModelType); + return new IndicatorBasedOptionPriceModel(pricingModelType, pricingModelType); } /// From e2f087107908b478b66a56cc61665524540acdd5 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 18 Feb 2026 17:21:18 -0400 Subject: [PATCH 16/18] Fix unit tests --- Tests/Indicators/IndicatorBasedOptionPriceModelTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Indicators/IndicatorBasedOptionPriceModelTests.cs b/Tests/Indicators/IndicatorBasedOptionPriceModelTests.cs index 07911612ca7c..c549ae37901a 100644 --- a/Tests/Indicators/IndicatorBasedOptionPriceModelTests.cs +++ b/Tests/Indicators/IndicatorBasedOptionPriceModelTests.cs @@ -27,8 +27,8 @@ 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)] + [TestCase(true, 6.05391914652262, 0.3564563, 0.7560627, 0.0430897, 0.0662474, -4.3932945 * 365, 0.0000902)] + [TestCase(false, 5.05413609164657, 0.1428964, 0.9574846, 0.0311305, 0.0205564, -0.4502054 * 365, 0.0000057)] public void WorksWithAndWithoutMirrorContract([Values] bool withMirrorContract, decimal expectedTheoreticalPrice, decimal expectedIv, decimal expectedDelta, decimal expectedGamma, decimal expectedVega, decimal expectedTheta, decimal expectedRho) From 1d8d9306644bc1189cf7f53181306e7192e5cb16 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 19 Feb 2026 13:10:19 -0400 Subject: [PATCH 17/18] Move QL models to OptionPriceModels.QuantLib.* --- .../BasicTemplateOptionsHistoryAlgorithm.cs | 4 +- ...nCallITMGreeksExpiryRegressionAlgorithm.cs | 2 +- ...ptionExpiryDateTodayRegressionAlgorithm.cs | 2 +- ...portedAmericanOptionRegressionAlgorithm.cs | 2 +- ...portedAmericanOptionRegressionAlgorithm.cs | 2 +- ...portedEuropeanOptionRegressionAlgorithm.cs | 2 +- .../BasicTemplateOptionsHistoryAlgorithm.py | 2 +- ...portedAmericanOptionRegressionAlgorithm.py | 2 +- ...portedAmericanOptionRegressionAlgorithm.py | 2 +- ...portedEuropeanOptionRegressionAlgorithm.py | 2 +- .../Option/OptionPriceModels.QuantLib.cs | 191 ++++++++++++++++++ Common/Securities/Option/OptionPriceModels.cs | 140 +------------ .../Securities/OptionPriceModelTests.cs | 10 +- .../RandomDataGeneratorTests.cs | 4 +- .../RandomDataGeneratorProgram.cs | 2 +- 15 files changed, 215 insertions(+), 154 deletions(-) create mode 100644 Common/Securities/Option/OptionPriceModels.QuantLib.cs diff --git a/Algorithm.CSharp/BasicTemplateOptionsHistoryAlgorithm.cs b/Algorithm.CSharp/BasicTemplateOptionsHistoryAlgorithm.cs index 058bb0072c20..c280801f03aa 100644 --- a/Algorithm.CSharp/BasicTemplateOptionsHistoryAlgorithm.cs +++ b/Algorithm.CSharp/BasicTemplateOptionsHistoryAlgorithm.cs @@ -1,4 +1,4 @@ -/* +/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * @@ -47,7 +47,7 @@ public override void Initialize() // set the pricing model for Greeks and volatility // find more pricing models https://www.quantconnect.com/lean/documentation/topic27704.html - option.PriceModel = OptionPriceModels.CrankNicolsonFD(); + option.PriceModel = OptionPriceModels.BlackScholes(); // set the warm-up period for the pricing model SetWarmup(TimeSpan.FromDays(4)); // set the benchmark to be the initial cash diff --git a/Algorithm.CSharp/FutureOptionCallITMGreeksExpiryRegressionAlgorithm.cs b/Algorithm.CSharp/FutureOptionCallITMGreeksExpiryRegressionAlgorithm.cs index 12a2775e3bbb..0e680bf9f80d 100644 --- a/Algorithm.CSharp/FutureOptionCallITMGreeksExpiryRegressionAlgorithm.cs +++ b/Algorithm.CSharp/FutureOptionCallITMGreeksExpiryRegressionAlgorithm.cs @@ -61,7 +61,7 @@ public override void Initialize() .Take(1) .Single(), Resolution.Minute); - _esOption.PriceModel = OptionPriceModels.BjerksundStensland(); + _esOption.PriceModel = OptionPriceModels.QuantLib.BjerksundStensland(); _expectedOptionContract = QuantConnect.Symbol.CreateOption(_es19m20.Symbol, Market.CME, OptionStyle.American, OptionRight.Call, 3200m, new DateTime(2020, 6, 19)); if (_esOption.Symbol != _expectedOptionContract) diff --git a/Algorithm.CSharp/OptionExpiryDateTodayRegressionAlgorithm.cs b/Algorithm.CSharp/OptionExpiryDateTodayRegressionAlgorithm.cs index 2e8087514d1f..9ec7229e15f9 100644 --- a/Algorithm.CSharp/OptionExpiryDateTodayRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionExpiryDateTodayRegressionAlgorithm.cs @@ -42,7 +42,7 @@ public override void Initialize() { return universeFilter.IncludeWeeklys().Strikes(-2, +2).Expiration(0, 10); }); - option.PriceModel = OptionPriceModels.BaroneAdesiWhaley(); + option.PriceModel = OptionPriceModels.BlackScholes(); _optionSymbol = option.Symbol; SetWarmUp(TimeSpan.FromDays(3)); diff --git a/Algorithm.CSharp/OptionPriceModelForSupportedAmericanOptionRegressionAlgorithm.cs b/Algorithm.CSharp/OptionPriceModelForSupportedAmericanOptionRegressionAlgorithm.cs index dbf8825d462f..7ab9ead8912e 100644 --- a/Algorithm.CSharp/OptionPriceModelForSupportedAmericanOptionRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionPriceModelForSupportedAmericanOptionRegressionAlgorithm.cs @@ -33,7 +33,7 @@ public override void Initialize() option.SetFilter(u => u.StandardsOnly().Strikes(-1, 1).Expiration(0, 35)); // BaroneAdesiWhaley model supports American style options - option.PriceModel = OptionPriceModels.BaroneAdesiWhaley(); + option.PriceModel = OptionPriceModels.QuantLib.BaroneAdesiWhaley(); SetWarmup(2, Resolution.Daily); diff --git a/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs b/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs index 49537220213f..d07222b471bd 100644 --- a/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.cs @@ -33,7 +33,7 @@ public override void Initialize() option.SetFilter(u => u.StandardsOnly().Strikes(-1, 1).Expiration(0, 35)); // QL BlackSholes model does not support American style options - option.PriceModel = QLOptionPriceModelProvider.Instance.GetOptionPriceModel(option.Symbol, OptionPricingModelType.BlackScholes); + option.PriceModel = OptionPriceModels.QuantLib.BlackScholes(); SetWarmup(2, Resolution.Daily); diff --git a/Algorithm.CSharp/OptionPriceModelForUnsupportedEuropeanOptionRegressionAlgorithm.cs b/Algorithm.CSharp/OptionPriceModelForUnsupportedEuropeanOptionRegressionAlgorithm.cs index 34d461372234..64aebae96899 100644 --- a/Algorithm.CSharp/OptionPriceModelForUnsupportedEuropeanOptionRegressionAlgorithm.cs +++ b/Algorithm.CSharp/OptionPriceModelForUnsupportedEuropeanOptionRegressionAlgorithm.cs @@ -32,7 +32,7 @@ public override void Initialize() var option = AddIndexOption("SPX", Resolution.Hour); // BaroneAdesiWhaley model does not support European style options - option.PriceModel = OptionPriceModels.BaroneAdesiWhaley(); + option.PriceModel = OptionPriceModels.QuantLib.BaroneAdesiWhaley(); SetWarmup(7, Resolution.Daily); diff --git a/Algorithm.Python/BasicTemplateOptionsHistoryAlgorithm.py b/Algorithm.Python/BasicTemplateOptionsHistoryAlgorithm.py index ee711d9ef110..6968b1338779 100644 --- a/Algorithm.Python/BasicTemplateOptionsHistoryAlgorithm.py +++ b/Algorithm.Python/BasicTemplateOptionsHistoryAlgorithm.py @@ -38,7 +38,7 @@ def initialize(self): # set the pricing model for Greeks and volatility # find more pricing models https://www.quantconnect.com/lean/documentation/topic27704.html - option.price_model = OptionPriceModels.crank_nicolson_fd() + option.price_model = OptionPriceModels.black_scholes() # set the warm-up period for the pricing model self.set_warm_up(TimeSpan.from_days(4)) # set the benchmark to be the initial cash diff --git a/Algorithm.Python/OptionPriceModelForSupportedAmericanOptionRegressionAlgorithm.py b/Algorithm.Python/OptionPriceModelForSupportedAmericanOptionRegressionAlgorithm.py index a87ff4cbe450..f65beb8801c3 100644 --- a/Algorithm.Python/OptionPriceModelForSupportedAmericanOptionRegressionAlgorithm.py +++ b/Algorithm.Python/OptionPriceModelForSupportedAmericanOptionRegressionAlgorithm.py @@ -27,7 +27,7 @@ def initialize(self): option.set_filter(lambda u: u.standards_only().strikes(-1, 1).expiration(0, 35)) # BaroneAdesiWhaley model supports American style options - option.price_model = OptionPriceModels.barone_adesi_whaley() + option.price_model = OptionPriceModels.QuantLib.barone_adesi_whaley() self.set_warmup(2, Resolution.DAILY) diff --git a/Algorithm.Python/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.py b/Algorithm.Python/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.py index ca785c7d110a..2c6e8dccdfde 100644 --- a/Algorithm.Python/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.py +++ b/Algorithm.Python/OptionPriceModelForUnsupportedAmericanOptionRegressionAlgorithm.py @@ -27,7 +27,7 @@ def initialize(self): option.set_filter(lambda u: u.standards_only().strikes(-1, 1).expiration(0, 35)) # BlackSholes model does not support American style options - option.price_model = QLOptionPriceModelProvider.INSTANCE.get_option_price_model(option.symbol, OptionPricingModelType.BLACK_SCHOLES) + option.price_model = OptionPriceModels.QuantLib.black_scholes() self.set_warmup(2, Resolution.DAILY) diff --git a/Algorithm.Python/OptionPriceModelForUnsupportedEuropeanOptionRegressionAlgorithm.py b/Algorithm.Python/OptionPriceModelForUnsupportedEuropeanOptionRegressionAlgorithm.py index ea2210f6bc13..d363b7972f06 100644 --- a/Algorithm.Python/OptionPriceModelForUnsupportedEuropeanOptionRegressionAlgorithm.py +++ b/Algorithm.Python/OptionPriceModelForUnsupportedEuropeanOptionRegressionAlgorithm.py @@ -25,7 +25,7 @@ def initialize(self): option = self.add_index_option("SPX", Resolution.HOUR) # BaroneAdesiWhaley model does not support European style options - option.price_model = OptionPriceModels.barone_adesi_whaley() + option.price_model = OptionPriceModels.QuantLib.barone_adesi_whaley() self.set_warmup(7, Resolution.DAILY) diff --git a/Common/Securities/Option/OptionPriceModels.QuantLib.cs b/Common/Securities/Option/OptionPriceModels.QuantLib.cs new file mode 100644 index 000000000000..c074bf2baaa2 --- /dev/null +++ b/Common/Securities/Option/OptionPriceModels.QuantLib.cs @@ -0,0 +1,191 @@ +/* + * 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 QLNet; +using System; +using System.Linq; +using Fasterflect; + +namespace QuantConnect.Securities.Option +{ + using PricingEngineFuncEx = Func; + + public static partial class OptionPriceModels + { + /// + /// Static class contains definitions of major option pricing models that can be used in LEAN, + /// based on QuantLib implementations. + /// + /// + /// To introduce particular model into algorithm add the following line to the algorithm's Initialize() method: + /// + /// option.PriceModel = OptionPriceModels.QuantLib.BjerksundStensland(); // Option pricing model of choice + /// + /// + public static class QuantLib + { + private const int _timeStepsFD = 100; + + /// + /// Creates pricing engine by engine type name. + /// + /// QL price engine name + /// The risk free rate + /// List of option styles supported by the pricing model. It defaults to both American and European option styles + /// New option price model instance of specific engine + public static IOptionPriceModel Create(string priceEngineName, decimal riskFree, OptionStyle[] allowedOptionStyles = null) + { + var type = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic) + .SelectMany(a => a.GetTypes()) + .Where(s => s.Implements(typeof(IPricingEngine))) + .FirstOrDefault(t => t.FullName?.EndsWith(priceEngineName, StringComparison.InvariantCulture) == true); + + return new QLOptionPriceModel(process => (IPricingEngine)Activator.CreateInstance(type, process), + riskFreeRateEstimator: new ConstantQLRiskFreeRateEstimator(riskFree), + allowedOptionStyles: allowedOptionStyles); + } + + /// + /// Pricing engine for European vanilla options using analytical formula. + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_analytic_european_engine.html + /// + /// New option price model instance + public static IOptionPriceModel BlackScholes() + { + return QLOptionPriceModelProvider.Instance.GetOptionPriceModel(Symbol.Empty, Indicators.OptionPricingModelType.BlackScholes); + } + + /// + /// Barone-Adesi and Whaley pricing engine for American options (1987) + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_barone_adesi_whaley_approximation_engine.html + /// + /// New option price model instance + public static IOptionPriceModel BaroneAdesiWhaley() + { + return new QLOptionPriceModel(process => new BaroneAdesiWhaleyApproximationEngine(process), + allowedOptionStyles: new[] { OptionStyle.American }); + } + + /// + /// Bjerksund and Stensland pricing engine for American options (1993) + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_bjerksund_stensland_approximation_engine.html + /// + /// New option price model instance + public static IOptionPriceModel BjerksundStensland() + { + return new QLOptionPriceModel(process => new BjerksundStenslandApproximationEngine(process), + allowedOptionStyles: new[] { OptionStyle.American }); + } + + /// + /// Pricing engine for European vanilla options using integral approach. + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_integral_engine.html + /// + /// New option price model instance + public static IOptionPriceModel Integral() + { + return new QLOptionPriceModel(process => new IntegralEngine(process), + allowedOptionStyles: new[] { OptionStyle.European }); + } + + /// + /// Pricing engine for European and American options using finite-differences. + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html + /// + /// New option price model instance + public static IOptionPriceModel CrankNicolsonFD() + { + PricingEngineFuncEx pricingEngineFunc = (symbol, process) => + symbol.ID.OptionStyle == OptionStyle.American + ? new FDAmericanEngine(process, _timeStepsFD, _timeStepsFD - 1) + : new FDEuropeanEngine(process, _timeStepsFD, _timeStepsFD - 1); + + return new QLOptionPriceModel(pricingEngineFunc); + } + + /// + /// Pricing engine for European and American vanilla options using binomial trees. Jarrow-Rudd model. + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html + /// + /// New option price model instance + public static IOptionPriceModel BinomialJarrowRudd() + { + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); + } + + + /// + /// Pricing engine for European and American vanilla options using binomial trees. Cox-Ross-Rubinstein(CRR) model. + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html + /// + /// New option price model instance + public static IOptionPriceModel BinomialCoxRossRubinstein() + { + return QLOptionPriceModelProvider.Instance.GetOptionPriceModel(Symbol.Empty, Indicators.OptionPricingModelType.BinomialCoxRossRubinstein); + } + + /// + /// Pricing engine for European and American vanilla options using binomial trees. Additive Equiprobabilities model. + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html + /// + /// New option price model instance + public static IOptionPriceModel AdditiveEquiprobabilities() + { + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); + } + + /// + /// Pricing engine for European and American vanilla options using binomial trees. Trigeorgis model. + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html + /// + /// New option price model instance + public static IOptionPriceModel BinomialTrigeorgis() + { + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); + } + + /// + /// Pricing engine for European and American vanilla options using binomial trees. Tian model. + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html + /// + /// New option price model instance + public static IOptionPriceModel BinomialTian() + { + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); + } + + /// + /// Pricing engine for European and American vanilla options using binomial trees. Leisen-Reimer model. + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html + /// + /// New option price model instance + public static IOptionPriceModel BinomialLeisenReimer() + { + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); + } + + /// + /// Pricing engine for European and American vanilla options using binomial trees. Joshi model. + /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html + /// + /// New option price model instance + public static IOptionPriceModel BinomialJoshi() + { + return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); + } + } + } +} diff --git a/Common/Securities/Option/OptionPriceModels.cs b/Common/Securities/Option/OptionPriceModels.cs index ba23f3f3f44f..84ee14454b21 100644 --- a/Common/Securities/Option/OptionPriceModels.cs +++ b/Common/Securities/Option/OptionPriceModels.cs @@ -15,50 +15,30 @@ using QLNet; using System; -using System.Linq; -using Fasterflect; namespace QuantConnect.Securities.Option { - using PricingEngineFuncEx = Func; - /// /// Static class contains definitions of major option pricing models that can be used in LEAN /// /// /// To introduce particular model into algorithm add the following line to the algorithm's Initialize() method: /// - /// option.PriceModel = OptionPriceModels.BjerksundStensland(); // Option pricing model of choice + /// option.PriceModel = OptionPriceModels.BlackScholes(); // Option pricing model of choice /// /// - public static class OptionPriceModels + public static partial class OptionPriceModels { - private const int _timeStepsFD = 100; - /// /// Default option price model provider used by LEAN when creating price models. /// internal static IOptionPriceModelProvider DefaultPriceModelProvider { get; set; } = QLOptionPriceModelProvider.Instance; /// - /// Creates pricing engine by engine type name. + /// Null pricing engine that returns the current price as the option theoretical price. + /// It will also set the option Greeks and implied volatility to zero, effectively disabling the pricing. /// - /// QL price engine name - /// The risk free rate - /// List of option styles supported by the pricing model. It defaults to both American and European option styles - /// New option price model instance of specific engine - public static IOptionPriceModel Create(string priceEngineName, decimal riskFree, OptionStyle[] allowedOptionStyles = null) - { - var type = AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic) - .SelectMany(a => a.GetTypes()) - .Where(s => s.Implements(typeof(IPricingEngine))) - .FirstOrDefault(t => t.FullName?.EndsWith(priceEngineName, StringComparison.InvariantCulture) == true); - - return new QLOptionPriceModel(process => (IPricingEngine)Activator.CreateInstance(type, process), - riskFreeRateEstimator: new ConstantQLRiskFreeRateEstimator(riskFree), - allowedOptionStyles: allowedOptionStyles); - } + public static IOptionPriceModel Null { get; } = new CurrentPriceOptionPriceModel(); /// /// Pricing engine for Black-Scholes model. @@ -69,65 +49,6 @@ public static IOptionPriceModel BlackScholes() return DefaultPriceModelProvider.GetOptionPriceModel(Symbol.Empty, Indicators.OptionPricingModelType.BlackScholes); } - /// - /// Barone-Adesi and Whaley pricing engine for American options (1987) - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_barone_adesi_whaley_approximation_engine.html - /// - /// New option price model instance - public static IOptionPriceModel BaroneAdesiWhaley() - { - return new QLOptionPriceModel(process => new BaroneAdesiWhaleyApproximationEngine(process), - allowedOptionStyles: new[] { OptionStyle.American }); - } - - /// - /// Bjerksund and Stensland pricing engine for American options (1993) - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_bjerksund_stensland_approximation_engine.html - /// - /// New option price model instance - public static IOptionPriceModel BjerksundStensland() - { - return new QLOptionPriceModel(process => new BjerksundStenslandApproximationEngine(process), - allowedOptionStyles: new[] { OptionStyle.American }); - } - - /// - /// Pricing engine for European vanilla options using integral approach. - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_integral_engine.html - /// - /// New option price model instance - public static IOptionPriceModel Integral() - { - return new QLOptionPriceModel(process => new IntegralEngine(process), - allowedOptionStyles: new[] { OptionStyle.European }); - } - - /// - /// Pricing engine for European and American options using finite-differences. - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html - /// - /// New option price model instance - public static IOptionPriceModel CrankNicolsonFD() - { - PricingEngineFuncEx pricingEngineFunc = (symbol, process) => - symbol.ID.OptionStyle == OptionStyle.American - ? new FDAmericanEngine(process, _timeStepsFD, _timeStepsFD - 1) - : new FDEuropeanEngine(process, _timeStepsFD, _timeStepsFD - 1); - - return new QLOptionPriceModel(pricingEngineFunc); - } - - /// - /// Pricing engine for European and American vanilla options using binomial trees. Jarrow-Rudd model. - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html - /// - /// New option price model instance - public static IOptionPriceModel BinomialJarrowRudd() - { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); - } - - /// /// Pricing engine for Cox-Ross-Rubinstein (CRR) model. /// @@ -136,56 +57,5 @@ public static IOptionPriceModel BinomialCoxRossRubinstein() { return DefaultPriceModelProvider.GetOptionPriceModel(Symbol.Empty, Indicators.OptionPricingModelType.BinomialCoxRossRubinstein); } - - /// - /// Pricing engine for European and American vanilla options using binomial trees. Additive Equiprobabilities model. - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html - /// - /// New option price model instance - public static IOptionPriceModel AdditiveEquiprobabilities() - { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); - } - - /// - /// Pricing engine for European and American vanilla options using binomial trees. Trigeorgis model. - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html - /// - /// New option price model instance - public static IOptionPriceModel BinomialTrigeorgis() - { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); - } - - /// - /// Pricing engine for European and American vanilla options using binomial trees. Tian model. - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html - /// - /// New option price model instance - public static IOptionPriceModel BinomialTian() - { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); - } - - /// - /// Pricing engine for European and American vanilla options using binomial trees. Leisen-Reimer model. - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html - /// - /// New option price model instance - public static IOptionPriceModel BinomialLeisenReimer() - { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); - } - - /// - /// Pricing engine for European and American vanilla options using binomial trees. Joshi model. - /// QuantLib reference: http://quantlib.org/reference/class_quant_lib_1_1_f_d_european_engine.html - /// - /// New option price model instance - public static IOptionPriceModel BinomialJoshi() - { - return new QLOptionPriceModel(process => new BinomialVanillaEngine(process, QLOptionPriceModelProvider.TimeStepsBinomial)); - } - } } diff --git a/Tests/Common/Securities/OptionPriceModelTests.cs b/Tests/Common/Securities/OptionPriceModelTests.cs index 3a0abaaa8656..1684b01fc4ef 100644 --- a/Tests/Common/Securities/OptionPriceModelTests.cs +++ b/Tests/Common/Securities/OptionPriceModelTests.cs @@ -221,7 +221,7 @@ public void BaroneAdesiWhaleyPortfolioTest() var optionCall = GetOption(SPY_C_192_Feb19_2016E, equity, tz); optionCall.SetMarketPrice(new Tick { Value = price }); - var priceModel = OptionPriceModels.BaroneAdesiWhaley(); + var priceModel = OptionPriceModels.QuantLib.BaroneAdesiWhaley(); var results = priceModel.Evaluate(new OptionPriceModelParameters(optionCall, null, contract)); var callPrice = results.TheoreticalPrice; @@ -254,7 +254,7 @@ public void EvaluationDateWorksInPortfolioTest() var optionCall = GetOption(SPY_C_192_Feb19_2016E, equity, tz); optionCall.SetMarketPrice(new Tick { Value = price }); - var priceModel = OptionPriceModels.BaroneAdesiWhaley(); + var priceModel = OptionPriceModels.QuantLib.BaroneAdesiWhaley(); var results = priceModel.Evaluate(new OptionPriceModelParameters(optionCall, null, contract)); var callPrice1 = results.TheoreticalPrice; @@ -273,7 +273,7 @@ public void CreatesOptionPriceModelByName(string priceEngineName) IOptionPriceModel priceModel = null; Assert.DoesNotThrow(() => { - priceModel = OptionPriceModels.Create(priceEngineName, 0.01m); + priceModel = OptionPriceModels.QuantLib.Create(priceEngineName, 0.01m); }); Assert.NotNull(priceModel); @@ -297,7 +297,7 @@ public void GreekApproximationTest() var optionPut = GetOption(Symbols.SPY_P_192_Feb19_2016, equity, tz); optionPut.SetMarketPrice(new Tick { Value = price }); - var priceModel = (QLOptionPriceModel)OptionPriceModels.CrankNicolsonFD(); + var priceModel = (QLOptionPriceModel)OptionPriceModels.QuantLib.CrankNicolsonFD(); priceModel.EnableGreekApproximation = false; var results = priceModel.Evaluate(optionPut, null, contract); @@ -307,7 +307,7 @@ public void GreekApproximationTest() Assert.AreEqual(greeks.Rho, 0); Assert.AreEqual(greeks.Vega, 0); - priceModel = (QLOptionPriceModel)OptionPriceModels.CrankNicolsonFD(); + priceModel = (QLOptionPriceModel)OptionPriceModels.QuantLib.CrankNicolsonFD(); priceModel.EnableGreekApproximation = true; results = priceModel.Evaluate(optionPut, null, contract); diff --git a/Tests/ToolBox/RandomDataGenerator/RandomDataGeneratorTests.cs b/Tests/ToolBox/RandomDataGenerator/RandomDataGeneratorTests.cs index 3cb3559c0a1e..9371d2cb4892 100644 --- a/Tests/ToolBox/RandomDataGenerator/RandomDataGeneratorTests.cs +++ b/Tests/ToolBox/RandomDataGenerator/RandomDataGeneratorTests.cs @@ -242,7 +242,7 @@ private static QuantConnect.ToolBox.RandomDataGenerator.RandomDataGenerator GetG // from settings if (security is Option option) { - option.PriceModel = OptionPriceModels.Create(settings.OptionPriceEngineName, + option.PriceModel = OptionPriceModels.QuantLib.Create(settings.OptionPriceEngineName, _interestRateProvider.GetRiskFreeRate(settings.Start, settings.End)); } })), @@ -281,7 +281,7 @@ private static SecurityService GetSecurityService(RandomDataGeneratorSettings se // from settings if (security is Option option) { - option.PriceModel = OptionPriceModels.Create(settings.OptionPriceEngineName, + option.PriceModel = OptionPriceModels.QuantLib.Create(settings.OptionPriceEngineName, _interestRateProvider.GetRiskFreeRate(settings.Start, settings.End)); } })), diff --git a/ToolBox/RandomDataGenerator/RandomDataGeneratorProgram.cs b/ToolBox/RandomDataGenerator/RandomDataGeneratorProgram.cs index 91f2937f01b6..4adbb6049d91 100644 --- a/ToolBox/RandomDataGenerator/RandomDataGeneratorProgram.cs +++ b/ToolBox/RandomDataGenerator/RandomDataGeneratorProgram.cs @@ -102,7 +102,7 @@ List tickers // from settings if (security is Option option) { - option.PriceModel = OptionPriceModels.Create(settings.OptionPriceEngineName, + option.PriceModel = OptionPriceModels.QuantLib.Create(settings.OptionPriceEngineName, _interestRateProvider.GetRiskFreeRate(settings.Start, settings.End)); } })), From 5eb1fae893413f6ae307da063df2f111e8f16089 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 19 Feb 2026 13:42:09 -0400 Subject: [PATCH 18/18] Add forward tree helper method --- Common/Securities/Option/OptionPriceModels.cs | 17 ++++++++++++++--- .../Common/Securities/OptionPriceModelTests.cs | 8 ++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Common/Securities/Option/OptionPriceModels.cs b/Common/Securities/Option/OptionPriceModels.cs index 84ee14454b21..2001ac884a57 100644 --- a/Common/Securities/Option/OptionPriceModels.cs +++ b/Common/Securities/Option/OptionPriceModels.cs @@ -13,7 +13,6 @@ * limitations under the License. */ -using QLNet; using System; namespace QuantConnect.Securities.Option @@ -32,13 +31,16 @@ public static partial class OptionPriceModels /// /// Default option price model provider used by LEAN when creating price models. /// - internal static IOptionPriceModelProvider DefaultPriceModelProvider { get; set; } = QLOptionPriceModelProvider.Instance; + internal static IOptionPriceModelProvider DefaultPriceModelProvider { get; set; } /// /// Null pricing engine that returns the current price as the option theoretical price. /// It will also set the option Greeks and implied volatility to zero, effectively disabling the pricing. /// - public static IOptionPriceModel Null { get; } = new CurrentPriceOptionPriceModel(); + public static IOptionPriceModel Null() + { + return new CurrentPriceOptionPriceModel(); + } /// /// Pricing engine for Black-Scholes model. @@ -57,5 +59,14 @@ public static IOptionPriceModel BinomialCoxRossRubinstein() { return DefaultPriceModelProvider.GetOptionPriceModel(Symbol.Empty, Indicators.OptionPricingModelType.BinomialCoxRossRubinstein); } + + /// + /// Pricing engine for forward binomial tree model. + /// + /// New option price model instance + public static IOptionPriceModel ForwardTree() + { + return DefaultPriceModelProvider.GetOptionPriceModel(Symbol.Empty, Indicators.OptionPricingModelType.ForwardTree); + } } } diff --git a/Tests/Common/Securities/OptionPriceModelTests.cs b/Tests/Common/Securities/OptionPriceModelTests.cs index 1684b01fc4ef..e02d4e42b042 100644 --- a/Tests/Common/Securities/OptionPriceModelTests.cs +++ b/Tests/Common/Securities/OptionPriceModelTests.cs @@ -36,8 +36,8 @@ namespace QuantConnect.Tests.Common [TestFixture] public class OptionPriceModelTests { - [SetUp] - public void SetUp() + [OneTimeSetUp] + public void OneTimeSetUp() { OptionPriceModels.DefaultPriceModelProvider = QLOptionPriceModelProvider.Instance; } @@ -401,7 +401,7 @@ public void ThrowsIfOptionStyleIsNotSupportedByQLPricingModel(string qlModelName optionPut.SetMarketPrice(new Tick { Value = 7m }); // dummy non-zero price // running evaluation - var priceModel = (IOptionPriceModel)typeof(OptionPriceModels).GetMethod(qlModelName).Invoke(null, new object[] { }); + var priceModel = (IOptionPriceModel)typeof(OptionPriceModels.QuantLib).GetMethod(qlModelName).Invoke(null, new object[] { }); TestDelegate call = () => priceModel.Evaluate(new OptionPriceModelParameters(optionCall, null, contractCall)); TestDelegate put = () => priceModel.Evaluate(new OptionPriceModelParameters(optionPut, null, contractPut)); @@ -829,7 +829,7 @@ private void MatchesIBGreeksTest(Symbol symbol, Symbol optionSymbol, string file // setting up option var contract = GetOptionContract(optionSymbol, symbol, evaluationDate); var option = GetOption(optionSymbol, equity, tz); - var priceModel = (IOptionPriceModel)typeof(OptionPriceModels).GetMethod(qlModelName).Invoke(null, new object[] { }); + var priceModel = (IOptionPriceModel)typeof(OptionPriceModels.QuantLib).GetMethod(qlModelName).Invoke(null, new object[] { }); // Get test data var data = File.ReadAllLines($"TestData/greeks/{filename}.csv")