/* * 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 QuantConnect.Data; using QuantConnect.Logging; using QuantConnect.Data.Market; using System.Collections.Generic; namespace QuantConnect.Securities.Option { using PricingEngineFunc = Func; using PricingEngineFuncEx = Func; /// /// Provides QuantLib(QL) implementation of to support major option pricing models, available in QL. /// public class QLOptionPriceModel : IOptionPriceModel { private static readonly OptionStyle[] _defaultAllowedOptionStyles = { OptionStyle.European, OptionStyle.American }; private static readonly IQLUnderlyingVolatilityEstimator _defaultUnderlyingVolEstimator = new ConstantQLUnderlyingVolatilityEstimator(); private static readonly IQLRiskFreeRateEstimator _defaultRiskFreeRateEstimator = new FedRateQLRiskFreeRateEstimator(); private static readonly IQLDividendYieldEstimator _defaultDividendYieldEstimator = new ConstantQLDividendYieldEstimator(); private readonly IQLUnderlyingVolatilityEstimator _underlyingVolEstimator; private readonly IQLDividendYieldEstimator _dividendYieldEstimator; private readonly IQLRiskFreeRateEstimator _riskFreeRateEstimator; private readonly PricingEngineFuncEx _pricingEngineFunc; /// /// When enabled, approximates Greeks if corresponding pricing model didn't calculate exact numbers. /// The default value is true. /// public bool EnableGreekApproximation { get; set; } = true; /// /// True if volatility model is warmed up, i.e. has generated volatility value different from zero, otherwise false. /// public bool VolatilityEstimatorWarmedUp => _underlyingVolEstimator.IsReady; /// /// List of option styles supported by the pricing model. /// By default, both American and European option styles are supported. /// public IReadOnlyCollection AllowedOptionStyles { get; } /// /// Method constructs QuantLib option price model with necessary estimators of underlying volatility, risk free rate, and underlying dividend yield /// /// Function modeled stochastic process, and returns new pricing engine to run calculations for that option /// The underlying volatility estimator /// The risk free rate estimator /// The underlying dividend yield estimator /// List of option styles supported by the pricing model. It defaults to both American and European option styles public QLOptionPriceModel(PricingEngineFunc pricingEngineFunc, IQLUnderlyingVolatilityEstimator underlyingVolEstimator = null, IQLRiskFreeRateEstimator riskFreeRateEstimator = null, IQLDividendYieldEstimator dividendYieldEstimator = null, OptionStyle[] allowedOptionStyles = null) : this((option, process) => pricingEngineFunc(process), underlyingVolEstimator, riskFreeRateEstimator, dividendYieldEstimator, allowedOptionStyles) {} /// /// Method constructs QuantLib option price model with necessary estimators of underlying volatility, risk free rate, and underlying dividend yield /// /// Function takes option and modeled stochastic process, and returns new pricing engine to run calculations for that option /// The underlying volatility estimator /// The risk free rate estimator /// The underlying dividend yield estimator /// List of option styles supported by the pricing model. It defaults to both American and European option styles public QLOptionPriceModel(PricingEngineFuncEx pricingEngineFunc, IQLUnderlyingVolatilityEstimator underlyingVolEstimator = null, IQLRiskFreeRateEstimator riskFreeRateEstimator = null, IQLDividendYieldEstimator dividendYieldEstimator = null, OptionStyle[] allowedOptionStyles = null) { _pricingEngineFunc = pricingEngineFunc; _underlyingVolEstimator = underlyingVolEstimator ?? _defaultUnderlyingVolEstimator; _riskFreeRateEstimator = riskFreeRateEstimator ?? _defaultRiskFreeRateEstimator; _dividendYieldEstimator = dividendYieldEstimator ?? _defaultDividendYieldEstimator; AllowedOptionStyles = allowedOptionStyles ?? _defaultAllowedOptionStyles; } /// /// Evaluates the specified option contract to compute a theoretical price, IV and greeks /// /// 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) { if (!AllowedOptionStyles.Contains(contract.Symbol.ID.OptionStyle)) { throw new ArgumentException($"{contract.Symbol.ID.OptionStyle} style options are not supported by option price model '{this.GetType().Name}'"); } try { // expired options have no price if (contract.Time.Date > contract.Expiry.Date) { if (Log.DebuggingEnabled) { Log.Debug($"QLOptionPriceModel.Evaluate(). Expired {contract.Symbol}. Time > Expiry: {contract.Time.Date} > {contract.Expiry.Date}"); } return OptionPriceModelResult.None; } var dayCounter = new Actual365Fixed(); var securityExchangeHours = security.Exchange.Hours; var maturityDate = AddDays(contract.Expiry.Date, Option.DefaultSettlementDays, securityExchangeHours); // Get time until maturity (in year) var maturity = dayCounter.yearFraction(contract.Time.Date, maturityDate); if (maturity < 0) { if (Log.DebuggingEnabled) { Log.Debug($"QLOptionPriceModel.Evaluate(). negative time ({maturity}) given for {contract.Symbol}. Time: {contract.Time.Date}. Maturity {maturityDate}"); } return OptionPriceModelResult.None; } // setting up option pricing parameters var optionSecurity = (Option)security; var premium = (double)optionSecurity.Price; var spot = (double)optionSecurity.Underlying.Price; if (spot <= 0d || premium <= 0d) { if (Log.DebuggingEnabled) { Log.Debug($"QLOptionPriceModel.Evaluate(). Non-positive prices for {contract.Symbol}. Premium: {premium}. Underlying price {spot}"); } return OptionPriceModelResult.None; } var calendar = new UnitedStates(); var settlementDate = AddDays(contract.Time.Date, Option.DefaultSettlementDays, securityExchangeHours); var underlyingQuoteValue = new SimpleQuote(spot); var dividendYieldValue = new SimpleQuote(_dividendYieldEstimator.Estimate(security, slice, contract)); var dividendYield = new Handle(new FlatForward(0, calendar, dividendYieldValue, dayCounter)); var riskFreeRateValue = new SimpleQuote((double)_riskFreeRateEstimator.Estimate(security, slice, contract)); var riskFreeRate = new Handle(new FlatForward(0, calendar, riskFreeRateValue, dayCounter)); // Get discount factor by dividend and risk free rate using the maturity var dividendDiscount = dividendYield.link.discount(maturity); var riskFreeDiscount = riskFreeRate.link.discount(maturity); var forwardPrice = spot * dividendDiscount / riskFreeDiscount; // Initial guess for volatility by Brenner and Subrahmanyam (1988) var initialGuess = Math.Sqrt(2 * Math.PI / maturity) * premium / spot; var underlyingVolEstimate = _underlyingVolEstimator.Estimate(security, slice, contract); // If the volatility estimator is not ready, we will use initial guess if (!_underlyingVolEstimator.IsReady) { underlyingVolEstimate = initialGuess; } var underlyingVolValue = new SimpleQuote(underlyingVolEstimate); var underlyingVol = new Handle(new BlackConstantVol(0, calendar, new Handle(underlyingVolValue), dayCounter)); // preparing stochastic process and payoff functions var stochasticProcess = new BlackScholesMertonProcess(new Handle(underlyingQuoteValue), dividendYield, riskFreeRate, underlyingVol); var payoff = new PlainVanillaPayoff(contract.Right == OptionRight.Call ? QLNet.Option.Type.Call : QLNet.Option.Type.Put, (double)contract.Strike); // creating option QL object var option = contract.Symbol.ID.OptionStyle == OptionStyle.American ? new VanillaOption(payoff, new AmericanExercise(settlementDate, maturityDate)) : new VanillaOption(payoff, new EuropeanExercise(maturityDate)); // preparing pricing engine QL object option.setPricingEngine(_pricingEngineFunc(contract.Symbol, stochasticProcess)); // Setting the evaluation date before running the calculations var evaluationDate = contract.Time.Date; SetEvaluationDate(evaluationDate); // running calculations var npv = EvaluateOption(option); BlackCalculator blackCalculator = null; // Calculate the Implied Volatility var impliedVol = 0d; try { SetEvaluationDate(evaluationDate); impliedVol = option.impliedVolatility(premium, stochasticProcess); } catch (Exception e) { // A Newton-Raphson optimization estimate of the implied volatility impliedVol = ImpliedVolatilityEstimation(premium, initialGuess, maturity, riskFreeDiscount, forwardPrice, payoff, out blackCalculator); if (Log.DebuggingEnabled) { var referenceDate = underlyingVol.link.referenceDate(); Log.Debug($"QLOptionPriceModel.Evaluate(). Cannot calculate Implied Volatility for {contract.Symbol}. Implied volatility from Newton-Raphson optimization: {impliedVol}. Premium: {premium}. Underlying price: {spot}. Initial guess volatility: {initialGuess}. Maturity: {maturity}. Risk Free: {riskFreeDiscount}. Forward price: {forwardPrice}. Data time: {evaluationDate}. Reference date: {referenceDate}. {e.Message} {e.StackTrace}"); } } // Update the Black Vol Term Structure with the Implied Volatility to improve Greek calculation // We assume that the underlying volatility model does not yield a good estimate and // other sources, e.g. Interactive Brokers, use the implied volatility to calculate the Greeks // After this operation, the Theoretical Price (NPV) will match the Premium, so we do not re-evalute // it and let users compare NPV and the Premium if they wish. underlyingVolValue.setValue(impliedVol); // function extracts QL greeks catching exception if greek is not generated by the pricing engine and reevaluates option to get numerical estimate of the seisitivity decimal tryGetGreekOrReevaluate(Func greek, Func black) { double result; var isApproximation = false; Exception exception = null; try { SetEvaluationDate(evaluationDate); result = greek(); } catch (Exception err) { exception = err; if (!EnableGreekApproximation) { return 0.0m; } if (blackCalculator == null) { // Define Black Calculator to calculate Greeks that are not defined by the option object // Some models do not evaluate all greeks under some circumstances (e.g. low dividend yield) // We override this restriction to calculate the Greeks directly with the BlackCalculator var vol = underlyingVol.link.blackVol(maturityDate, (double)contract.Strike); blackCalculator = CreateBlackCalculator(forwardPrice, riskFreeDiscount, vol, payoff); } isApproximation = true; result = black(blackCalculator); } if (result.IsNaNOrInfinity()) { if (Log.DebuggingEnabled) { var referenceDate = underlyingVol.link.referenceDate(); Log.Debug($"QLOptionPriceModel.Evaluate(). NaN or Infinity greek for {contract.Symbol}. Premium: {premium}. Underlying price: {spot}. Initial guess volatility: {initialGuess}. Maturity: {maturity}. Risk Free: {riskFreeDiscount}. Forward price: {forwardPrice}. Implied Volatility: {impliedVol}. Is Approximation? {isApproximation}. Data time: {evaluationDate}. Reference date: {referenceDate}. {exception?.Message} {exception?.StackTrace}"); } return 0m; } var value = result.SafeDecimalCast(); if (value == decimal.Zero && Log.DebuggingEnabled) { var referenceDate = underlyingVol.link.referenceDate(); Log.Debug($"QLOptionPriceModel.Evaluate(). Zero-value greek for {contract.Symbol}. Premium: {premium}. Underlying price: {spot}. Initial guess volatility: {initialGuess}. Maturity: {maturity}. Risk Free: {riskFreeDiscount}. Forward price: {forwardPrice}. Implied Volatility: {impliedVol}. Is Approximation? {isApproximation}. Data time: {evaluationDate}. Reference date: {referenceDate}. {exception?.Message} {exception?.StackTrace}"); return value; } return value; } // producing output with lazy calculations of greeks return new OptionPriceModelResult(npv, // EvaluateOption ensure it is not NaN or Infinity () => impliedVol.IsNaNOrInfinity() ? 0m : impliedVol.SafeDecimalCast(), () => new ModeledGreeks(() => tryGetGreekOrReevaluate(() => option.delta(), (black) => black.delta(spot)), () => tryGetGreekOrReevaluate(() => option.gamma(), (black) => black.gamma(spot)), () => tryGetGreekOrReevaluate(() => option.vega(), (black) => black.vega(maturity)) / 100, // per cent () => tryGetGreekOrReevaluate(() => option.theta(), (black) => black.theta(spot, maturity)), () => tryGetGreekOrReevaluate(() => option.rho(), (black) => black.rho(maturity)) / 100, // per cent () => tryGetGreekOrReevaluate(() => option.elasticity(), (black) => black.elasticity(spot)))); } catch (Exception err) { Log.Debug($"QLOptionPriceModel.Evaluate() error: {err.Message} {(Log.DebuggingEnabled ? err.StackTrace : string.Empty)} for {contract.Symbol}"); return OptionPriceModelResult.None; } } /// /// Runs option evaluation and logs exceptions /// /// /// private static decimal EvaluateOption(VanillaOption option) { try { var npv = option.NPV(); if (double.IsNaN(npv) || double.IsInfinity(npv)) return 0; // can return negative value in neighborhood of 0 return Math.Max(0, npv).SafeDecimalCast(); } catch (Exception err) { Log.Debug($"QLOptionPriceModel.EvaluateOption() error: {err.Message}"); return 0; } } /// /// An implied volatility approximation by Newton-Raphson method. Return 0 if result is not converged /// /// /// Orlando G, Taglialatela G. A review on implied volatility calculation. Journal of Computational and Applied Mathematics. 2017 Aug 15;320:202-20. /// https://www.sciencedirect.com/science/article/pii/S0377042717300602 /// /// current price of the option /// initial guess of the IV /// time till option contract expiry /// risk free rate discount factor /// future value of underlying price /// payoff structure of the option contract /// black calculator instance /// implied volatility estimation protected double ImpliedVolatilityEstimation(double price, double initialGuess, double timeTillExpiry, double riskFreeDiscount, double forwardPrice, PlainVanillaPayoff payoff, out BlackCalculator black) { // Set up the optimizer const double tolerance = 1e-3d; const double lowerBound = 1e-7d; const double upperBound = 4d; var iterRemain = 10; var error = double.MaxValue; var impliedVolEstimate = initialGuess; // Set up option calculator black = CreateBlackCalculator(forwardPrice, riskFreeDiscount, initialGuess, payoff); while (error > tolerance && iterRemain > 0) { var oldImpliedVol = impliedVolEstimate; // Set up calculator by previous IV estimate to get new theoretical price, vega and IV black = CreateBlackCalculator(forwardPrice, riskFreeDiscount, oldImpliedVol, payoff); impliedVolEstimate -= (black.value() - price) / black.vega(timeTillExpiry); if (impliedVolEstimate < lowerBound) { impliedVolEstimate = lowerBound; } else if (impliedVolEstimate > upperBound) { impliedVolEstimate = upperBound; } error = Math.Abs(impliedVolEstimate - oldImpliedVol) / impliedVolEstimate; iterRemain--; } if (iterRemain == 0) { if (Log.DebuggingEnabled) { Log.Debug("QLOptionPriceModel.ImpliedVolatilityEstimation() error: Implied Volatility approxiation did not converge, returning 0."); } return 0d; } return impliedVolEstimate; } /// /// Define Black Calculator to calculate Greeks that are not defined by the option object /// Some models do not evaluate all greeks under some circumstances (e.g. low dividend yield) /// We override this restriction to calculate the Greeks directly with the BlackCalculator /// private BlackCalculator CreateBlackCalculator(double forwardPrice, double riskFreeDiscount, double stdDev, PlainVanillaPayoff payoff) { return new BlackCalculator(payoff, forwardPrice, stdDev, riskFreeDiscount); } private static DateTime AddDays(DateTime date, int days, SecurityExchangeHours marketHours) { var forwardDate = date.AddDays(days); if (!marketHours.IsDateOpen(forwardDate)) { forwardDate = marketHours.GetNextTradingDay(forwardDate); } return forwardDate; } /// /// Set the evaluation date /// /// The current evaluation date private void SetEvaluationDate(DateTime evaluationDate) { if (Settings.evaluationDate().ToDateTime() != evaluationDate) { Settings.setEvaluationDate(evaluationDate); } } } }