/*
* 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;
using MathNet.Numerics.RootFinding;
using Python.Runtime;
using QuantConnect.Data;
using QuantConnect.Logging;
using QuantConnect.Python;
using QuantConnect.Util;
namespace QuantConnect.Indicators
{
///
/// Implied Volatility indicator that calculate the IV of an option using Black-Scholes Model
///
public class ImpliedVolatility : OptionIndicatorBase
{
private decimal _impliedVolatility;
private Func SmoothingFunction;
///
/// Initializes a new instance of the ImpliedVolatility class
///
/// The name of this indicator
/// The option to be tracked
/// Risk-free rate model
/// Dividend yield model
/// The mirror option for parity calculation
/// The option pricing model used to estimate IV
public ImpliedVolatility(string name, Symbol option, IRiskFreeInterestRateModel riskFreeRateModel, IDividendYieldModel dividendYieldModel, Symbol mirrorOption = null,
OptionPricingModelType? optionModel = null)
: base(name, option, riskFreeRateModel, dividendYieldModel, mirrorOption, optionModel)
{
if (mirrorOption != null)
{
// Default smoothing function will be assuming Law of One Price hold,
// so both call and put will have the same IV
// and using on OTM/ATM options to calculate the IV
// by assuming extra volatility coming from extrinsic value
SmoothingFunction = (impliedVol, mirrorImpliedVol) =>
{
if (Strike > UnderlyingPrice && Right == OptionRight.Put)
{
return mirrorImpliedVol;
}
else if (Strike < UnderlyingPrice && Right == OptionRight.Call)
{
return mirrorImpliedVol;
}
return impliedVol;
};
}
}
///
/// Initializes a new instance of the ImpliedVolatility class
///
/// The option to be tracked
/// Risk-free rate model
/// Dividend yield model
/// The mirror option for parity calculation
/// The option pricing model used to estimate IV
public ImpliedVolatility(Symbol option, IRiskFreeInterestRateModel riskFreeRateModel, IDividendYieldModel dividendYieldModel,
Symbol mirrorOption = null, OptionPricingModelType? optionModel = null)
: this($"IV({option},{mirrorOption},{riskFreeRateModel},{dividendYieldModel},{optionModel})", option, riskFreeRateModel,
dividendYieldModel, mirrorOption, optionModel)
{
}
///
/// Initializes a new instance of the ImpliedVolatility class
///
/// The name of this indicator
/// The option to be tracked
/// Risk-free rate model
/// Dividend yield model
/// The mirror option for parity calculation
/// The option pricing model used to estimate IV
public ImpliedVolatility(string name, Symbol option, PyObject riskFreeRateModel, PyObject dividendYieldModel, Symbol mirrorOption = null,
OptionPricingModelType? optionModel = null)
: this(name, option, RiskFreeInterestRateModelPythonWrapper.FromPyObject(riskFreeRateModel),
DividendYieldModelPythonWrapper.FromPyObject(dividendYieldModel), mirrorOption, optionModel)
{
}
///
/// Initializes a new instance of the ImpliedVolatility class
///
/// The option to be tracked
/// Risk-free rate model
/// Dividend yield model
/// The mirror option for parity calculation
/// The option pricing model used to estimate IV
public ImpliedVolatility(Symbol option, PyObject riskFreeRateModel, PyObject dividendYieldModel, Symbol mirrorOption = null,
OptionPricingModelType? optionModel = null)
: this($"IV({option},{mirrorOption},{riskFreeRateModel},{dividendYieldModel},{optionModel})", option,
riskFreeRateModel, dividendYieldModel, mirrorOption, optionModel)
{
}
///
/// Initializes a new instance of the ImpliedVolatility class
///
/// The name of this indicator
/// The option to be tracked
/// Risk-free rate model
/// Dividend yield, as a constant
/// The mirror option for parity calculation
/// The option pricing model used to estimate IV
public ImpliedVolatility(string name, Symbol option, IRiskFreeInterestRateModel riskFreeRateModel, decimal dividendYield = 0.0m, Symbol mirrorOption = null,
OptionPricingModelType? optionModel = null)
: this(name, option, riskFreeRateModel, new ConstantDividendYieldModel(dividendYield), mirrorOption, optionModel)
{
}
///
/// Initializes a new instance of the ImpliedVolatility class
///
/// The option to be tracked
/// Risk-free rate model
/// Dividend yield, as a constant
/// The mirror option for parity calculation
/// The option pricing model used to estimate IV
public ImpliedVolatility(Symbol option, IRiskFreeInterestRateModel riskFreeRateModel, decimal dividendYield = 0.0m, Symbol mirrorOption = null,
OptionPricingModelType? optionModel = null)
: this($"IV({option},{mirrorOption},{riskFreeRateModel},{dividendYield},{optionModel})", option, riskFreeRateModel, dividendYield,
mirrorOption, optionModel)
{
}
///
/// Initializes a new instance of the ImpliedVolatility class
///
/// The name of this indicator
/// The option to be tracked
/// Risk-free rate model
/// Dividend yield, as a constant
/// The mirror option for parity calculation
/// The option pricing model used to estimate IV
public ImpliedVolatility(string name, Symbol option, PyObject riskFreeRateModel, decimal dividendYield = 0.0m, Symbol mirrorOption = null,
OptionPricingModelType? optionModel = null)
: this(name, option, RiskFreeInterestRateModelPythonWrapper.FromPyObject(riskFreeRateModel),
new ConstantDividendYieldModel(dividendYield), mirrorOption, optionModel)
{
}
///
/// Initializes a new instance of the ImpliedVolatility class
///
/// The option to be tracked
/// Risk-free rate model
/// Dividend yield, as a constant
/// The mirror option for parity calculation
/// The option pricing model used to estimate IV
public ImpliedVolatility(Symbol option, PyObject riskFreeRateModel, decimal dividendYield = 0.0m, Symbol mirrorOption = null,
OptionPricingModelType? optionModel = null)
: this($"IV({option},{mirrorOption},{riskFreeRateModel},{dividendYield},{optionModel})", option, riskFreeRateModel,
dividendYield, mirrorOption, optionModel)
{
}
///
/// Initializes a new instance of the ImpliedVolatility class
///
/// The name of this indicator
/// The option to be tracked
/// Risk-free rate, as a constant
/// Dividend yield, as a constant
/// The mirror option for parity calculation
/// The option pricing model used to estimate IV
public ImpliedVolatility(string name, Symbol option, decimal riskFreeRate = 0.05m, decimal dividendYield = 0.0m, Symbol mirrorOption = null,
OptionPricingModelType? optionModel = null)
: this(name, option, new ConstantRiskFreeRateInterestRateModel(riskFreeRate), new ConstantDividendYieldModel(dividendYield), mirrorOption, optionModel)
{
}
///
/// Initializes a new instance of the ImpliedVolatility class
///
/// The option to be tracked
/// Risk-free rate, as a constant
/// Dividend yield, as a constant
/// The mirror option for parity calculation
/// The option pricing model used to estimate IV
public ImpliedVolatility(Symbol option, decimal riskFreeRate = 0.05m, decimal dividendYield = 0.0m, Symbol mirrorOption = null,
OptionPricingModelType? optionModel = null)
: this($"IV({option},{mirrorOption},{riskFreeRate},{dividendYield},{optionModel})", option, riskFreeRate,
dividendYield, mirrorOption, optionModel)
{
}
///
/// Set the smoothing function of IV, using both call and put IV value
///
/// the smoothing function
public void SetSmoothingFunction(Func function)
{
SmoothingFunction = function;
}
///
/// Set the smoothing function of IV, using both call and put IV value
///
/// the smoothing function
public void SetSmoothingFunction(PyObject function)
{
SmoothingFunction = PythonUtil.ToFunc(function);
}
///
/// Computes the next value
///
/// The input is returned unmodified.
protected override decimal ComputeIndicator()
{
var time = Price.Current.EndTime;
RiskFreeRate.Update(time, _riskFreeInterestRateModel.GetInterestRate(time));
DividendYield.Update(time, _dividendYieldModel.GetDividendYield(time, UnderlyingPrice.Current.Value));
var timeTillExpiry = Convert.ToDecimal(OptionGreekIndicatorsHelper.TimeTillExpiry(Expiry, time));
_impliedVolatility = CalculateIV(timeTillExpiry);
return _impliedVolatility;
}
// Calculate the theoretical option price
private static double TheoreticalPrice(double volatility, double spotPrice, double strikePrice, double timeTillExpiry, double riskFreeRate,
double dividendYield, OptionRight optionType, OptionPricingModelType? optionModel = null)
{
if (timeTillExpiry <= 0)
{
return 0;
}
return optionModel switch
{
// Binomial model also follows BSM process (log-normal)
OptionPricingModelType.BinomialCoxRossRubinstein => OptionGreekIndicatorsHelper.CRRTheoreticalPrice(volatility, spotPrice, strikePrice, timeTillExpiry, riskFreeRate, dividendYield, optionType),
OptionPricingModelType.ForwardTree => OptionGreekIndicatorsHelper.ForwardTreeTheoreticalPrice(volatility, spotPrice, strikePrice, timeTillExpiry, riskFreeRate, dividendYield, optionType),
_ => OptionGreekIndicatorsHelper.BlackTheoreticalPrice(volatility, spotPrice, strikePrice, timeTillExpiry, riskFreeRate, dividendYield, optionType),
};
}
///
/// Computes the IV of the option
///
/// the time until expiration in years
/// Smoothened IV of the option
protected virtual decimal CalculateIV(decimal timeTillExpiry)
{
var underlyingPrice = (double)UnderlyingPrice.Current.Value;
var strike = (double)Strike;
var timeTillExpiryDouble = (double)timeTillExpiry;
var riskFreeRate = (double)RiskFreeRate.Current.Value;
var dividendYield = (double)DividendYield.Current.Value;
var optionPrice = (double)Price.Current.Value;
var impliedVol = CalculateIV(OptionSymbol, strike, timeTillExpiryDouble, Right, optionPrice, underlyingPrice, riskFreeRate,
dividendYield, _optionModel);
if (UseMirrorContract)
{
var mirrorOptionPrice = (double)OppositePrice.Current.Value;
var mirrorImpliedVol = CalculateIV(_oppositeOptionSymbol, strike, timeTillExpiryDouble, _oppositeOptionSymbol.ID.OptionRight,
mirrorOptionPrice, underlyingPrice, riskFreeRate, dividendYield, _optionModel);
if (mirrorImpliedVol.HasValue)
{
if (impliedVol.HasValue)
{
// use 'SmoothingFunction' if both calculations succeeded
return SmoothingFunction(impliedVol.Value, mirrorImpliedVol.Value);
}
return mirrorImpliedVol.Value;
}
}
return impliedVol ?? 0;
}
private decimal? CalculateIV(Symbol optionSymbol, double strike, double timeTillExpiry, OptionRight right, double optionPrice, double underlyingPrice,
double riskFreeRate, double dividendYield, OptionPricingModelType optionModel)
{
GetRootFindingMethodParameters(optionSymbol, strike, timeTillExpiry, optionPrice, underlyingPrice, riskFreeRate, dividendYield,
optionModel, out var accuracy, out var lowerBound, out var upperBound);
decimal? impliedVol = null;
try
{
Func f = (vol) => TheoreticalPrice(vol, underlyingPrice, strike, timeTillExpiry, riskFreeRate, dividendYield, right, optionModel) - optionPrice;
impliedVol = Convert.ToDecimal(Brent.FindRoot(f, lowerBound, upperBound, accuracy, 100));
}
catch
{
Log.Error("ImpliedVolatility.CalculateIV(): Fail to converge, returning 0.");
}
return impliedVol;
}
private void GetRootFindingMethodParameters(Symbol optionSymbol, double strike, double timeTillExpiry, double optionPrice,
double underlyingPrice, double riskFreeRate, double dividendYield, OptionPricingModelType optionModel,
out double accuracy, out double lowerBound, out double upperBound)
{
// Set the accuracy as a factor of the option price when possible
accuracy = Math.Max(1e-4, 1e-4 * optionPrice);
lowerBound = 1e-7;
upperBound = 4.0;
// Use BSM as initial guess to get a better range for root finding
if (optionModel != OptionPricingModelType.BlackScholes)
{
var initialGuess = (double)(CalculateIV(optionSymbol, strike, timeTillExpiry, optionSymbol.ID.OptionRight, optionPrice,
underlyingPrice, riskFreeRate, dividendYield, OptionPricingModelType.BlackScholes) ?? 0);
if (initialGuess != 0)
{
lowerBound = Math.Max(lowerBound, initialGuess * 0.5);
upperBound = Math.Min(upperBound, initialGuess * 1.5);
}
}
}
}
}