440 lines
24 KiB
C#
440 lines
24 KiB
C#
/*
|
|
* 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<GeneralizedBlackScholesProcess, IPricingEngine>;
|
|
using PricingEngineFuncEx = Func<Symbol, GeneralizedBlackScholesProcess, IPricingEngine>;
|
|
|
|
/// <summary>
|
|
/// Provides QuantLib(QL) implementation of <see cref="IOptionPriceModel"/> to support major option pricing models, available in QL.
|
|
/// </summary>
|
|
public class QLOptionPriceModel : OptionPriceModel
|
|
{
|
|
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;
|
|
|
|
/// <summary>
|
|
/// When enabled, approximates Greeks if corresponding pricing model didn't calculate exact numbers.
|
|
/// The default value is true.
|
|
/// </summary>
|
|
public bool EnableGreekApproximation { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// True if volatility model is warmed up, i.e. has generated volatility value different from zero, otherwise false.
|
|
/// </summary>
|
|
public bool VolatilityEstimatorWarmedUp => _underlyingVolEstimator.IsReady;
|
|
|
|
/// <summary>
|
|
/// List of option styles supported by the pricing model.
|
|
/// By default, both American and European option styles are supported.
|
|
/// </summary>
|
|
public IReadOnlyCollection<OptionStyle> AllowedOptionStyles { get; }
|
|
|
|
/// <summary>
|
|
/// Method constructs QuantLib option price model with necessary estimators of underlying volatility, risk free rate, and underlying dividend yield
|
|
/// </summary>
|
|
/// <param name="pricingEngineFunc">Function modeled stochastic process, and returns new pricing engine to run calculations for that option</param>
|
|
/// <param name="underlyingVolEstimator">The underlying volatility estimator</param>
|
|
/// <param name="riskFreeRateEstimator">The risk free rate estimator</param>
|
|
/// <param name="dividendYieldEstimator">The underlying dividend yield estimator</param>
|
|
/// <param name="allowedOptionStyles">List of option styles supported by the pricing model. It defaults to both American and European option styles</param>
|
|
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)
|
|
{ }
|
|
/// <summary>
|
|
/// Method constructs QuantLib option price model with necessary estimators of underlying volatility, risk free rate, and underlying dividend yield
|
|
/// </summary>
|
|
/// <param name="pricingEngineFunc">Function takes option and modeled stochastic process, and returns new pricing engine to run calculations for that option</param>
|
|
/// <param name="underlyingVolEstimator">The underlying volatility estimator</param>
|
|
/// <param name="riskFreeRateEstimator">The risk free rate estimator</param>
|
|
/// <param name="dividendYieldEstimator">The underlying dividend yield estimator</param>
|
|
/// <param name="allowedOptionStyles">List of option styles supported by the pricing model. It defaults to both American and European option styles</param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates the specified option contract to compute a theoretical price, IV and greeks
|
|
/// </summary>
|
|
/// <param name="security">The option security object</param>
|
|
/// <param name="slice">The current data slice. This can be used to access other information
|
|
/// available to the algorithm</param>
|
|
/// <param name="contract">The option contract to evaluate</param>
|
|
/// <returns>An instance of <see cref="OptionPriceModelResult"/> containing the theoretical
|
|
/// price of the specified option contract</returns>
|
|
public override 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<YieldTermStructure>(new FlatForward(0, calendar, dividendYieldValue, dayCounter));
|
|
|
|
var riskFreeRateValue = new SimpleQuote((double)_riskFreeRateEstimator.Estimate(security, slice, contract));
|
|
var riskFreeRate = new Handle<YieldTermStructure>(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<BlackVolTermStructure>(new BlackConstantVol(0, calendar, new Handle<Quote>(underlyingVolValue), dayCounter));
|
|
|
|
// preparing stochastic process and payoff functions
|
|
var stochasticProcess = new BlackScholesMertonProcess(new Handle<Quote>(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<double> greek, Func<BlackCalculator, double> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluates the specified option contract to compute a theoretical price, IV and greeks
|
|
/// </summary>
|
|
/// <param name="parameters">A <see cref="OptionPriceModelParameters"/> object
|
|
/// containing the security, slice and contract</param>
|
|
/// <returns>An instance of <see cref="OptionPriceModelResult"/> containing the theoretical
|
|
/// price of the specified option contract</returns>
|
|
public override OptionPriceModelResult Evaluate(OptionPriceModelParameters parameters)
|
|
{
|
|
return Evaluate(parameters.Security, parameters.Slice, parameters.Contract);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs option evaluation and logs exceptions
|
|
/// </summary>
|
|
/// <param name="option"></param>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// An implied volatility approximation by Newton-Raphson method. Return 0 if result is not converged
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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
|
|
/// </remarks>
|
|
/// <param name="price">current price of the option</param>
|
|
/// <param name="initialGuess">initial guess of the IV</param>
|
|
/// <param name="timeTillExpiry">time till option contract expiry</param>
|
|
/// <param name="riskFreeDiscount">risk free rate discount factor</param>
|
|
/// <param name="forwardPrice">future value of underlying price</param>
|
|
/// <param name="payoff">payoff structure of the option contract</param>
|
|
/// <param name="black">black calculator instance</param>
|
|
/// <returns>implied volatility estimation</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the evaluation date
|
|
/// </summary>
|
|
/// <param name="evaluationDate">The current evaluation date</param>
|
|
private void SetEvaluationDate(DateTime evaluationDate)
|
|
{
|
|
if (Settings.evaluationDate().ToDateTime() != evaluationDate)
|
|
{
|
|
Settings.setEvaluationDate(evaluationDate);
|
|
}
|
|
}
|
|
}
|
|
}
|