Lean  $LEAN_TAG$
QLOptionPriceModel.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  *
15 */
16 
17 using QLNet;
18 using System;
19 using System.Linq;
20 using QuantConnect.Data;
21 using QuantConnect.Logging;
23 using System.Collections.Generic;
24 
26 {
27  using PricingEngineFunc = Func<GeneralizedBlackScholesProcess, IPricingEngine>;
28  using PricingEngineFuncEx = Func<Symbol, GeneralizedBlackScholesProcess, IPricingEngine>;
29 
30  /// <summary>
31  /// Provides QuantLib(QL) implementation of <see cref="IOptionPriceModel"/> to support major option pricing models, available in QL.
32  /// </summary>
34  {
35  private static readonly OptionStyle[] _defaultAllowedOptionStyles = { OptionStyle.European, OptionStyle.American };
36  private static readonly IQLUnderlyingVolatilityEstimator _defaultUnderlyingVolEstimator = new ConstantQLUnderlyingVolatilityEstimator();
37  private static readonly IQLRiskFreeRateEstimator _defaultRiskFreeRateEstimator = new FedRateQLRiskFreeRateEstimator();
38  private static readonly IQLDividendYieldEstimator _defaultDividendYieldEstimator = new ConstantQLDividendYieldEstimator();
39 
40  private readonly IQLUnderlyingVolatilityEstimator _underlyingVolEstimator;
41  private readonly IQLDividendYieldEstimator _dividendYieldEstimator;
42  private readonly IQLRiskFreeRateEstimator _riskFreeRateEstimator;
43  private readonly PricingEngineFuncEx _pricingEngineFunc;
44 
45  /// <summary>
46  /// When enabled, approximates Greeks if corresponding pricing model didn't calculate exact numbers.
47  /// The default value is true.
48  /// </summary>
49  public bool EnableGreekApproximation { get; set; } = true;
50 
51  /// <summary>
52  /// True if volatility model is warmed up, i.e. has generated volatility value different from zero, otherwise false.
53  /// </summary>
54  public bool VolatilityEstimatorWarmedUp => _underlyingVolEstimator.IsReady;
55 
56  /// <summary>
57  /// List of option styles supported by the pricing model.
58  /// By default, both American and European option styles are supported.
59  /// </summary>
60  public IReadOnlyCollection<OptionStyle> AllowedOptionStyles { get; }
61 
62  /// <summary>
63  /// Method constructs QuantLib option price model with necessary estimators of underlying volatility, risk free rate, and underlying dividend yield
64  /// </summary>
65  /// <param name="pricingEngineFunc">Function modeled stochastic process, and returns new pricing engine to run calculations for that option</param>
66  /// <param name="underlyingVolEstimator">The underlying volatility estimator</param>
67  /// <param name="riskFreeRateEstimator">The risk free rate estimator</param>
68  /// <param name="dividendYieldEstimator">The underlying dividend yield estimator</param>
69  /// <param name="allowedOptionStyles">List of option styles supported by the pricing model. It defaults to both American and European option styles</param>
70  public QLOptionPriceModel(PricingEngineFunc pricingEngineFunc,
71  IQLUnderlyingVolatilityEstimator underlyingVolEstimator = null,
72  IQLRiskFreeRateEstimator riskFreeRateEstimator = null,
73  IQLDividendYieldEstimator dividendYieldEstimator = null,
74  OptionStyle[] allowedOptionStyles = null)
75  : this((option, process) => pricingEngineFunc(process), underlyingVolEstimator, riskFreeRateEstimator, dividendYieldEstimator, allowedOptionStyles)
76  {}
77  /// <summary>
78  /// Method constructs QuantLib option price model with necessary estimators of underlying volatility, risk free rate, and underlying dividend yield
79  /// </summary>
80  /// <param name="pricingEngineFunc">Function takes option and modeled stochastic process, and returns new pricing engine to run calculations for that option</param>
81  /// <param name="underlyingVolEstimator">The underlying volatility estimator</param>
82  /// <param name="riskFreeRateEstimator">The risk free rate estimator</param>
83  /// <param name="dividendYieldEstimator">The underlying dividend yield estimator</param>
84  /// <param name="allowedOptionStyles">List of option styles supported by the pricing model. It defaults to both American and European option styles</param>
85  public QLOptionPriceModel(PricingEngineFuncEx pricingEngineFunc,
86  IQLUnderlyingVolatilityEstimator underlyingVolEstimator = null,
87  IQLRiskFreeRateEstimator riskFreeRateEstimator = null,
88  IQLDividendYieldEstimator dividendYieldEstimator = null,
89  OptionStyle[] allowedOptionStyles = null)
90  {
91  _pricingEngineFunc = pricingEngineFunc;
92  _underlyingVolEstimator = underlyingVolEstimator ?? _defaultUnderlyingVolEstimator;
93  _riskFreeRateEstimator = riskFreeRateEstimator ?? _defaultRiskFreeRateEstimator;
94  _dividendYieldEstimator = dividendYieldEstimator ?? _defaultDividendYieldEstimator;
95 
96  AllowedOptionStyles = allowedOptionStyles ?? _defaultAllowedOptionStyles;
97  }
98 
99  /// <summary>
100  /// Evaluates the specified option contract to compute a theoretical price, IV and greeks
101  /// </summary>
102  /// <param name="security">The option security object</param>
103  /// <param name="slice">The current data slice. This can be used to access other information
104  /// available to the algorithm</param>
105  /// <param name="contract">The option contract to evaluate</param>
106  /// <returns>An instance of <see cref="OptionPriceModelResult"/> containing the theoretical
107  /// price of the specified option contract</returns>
108  public OptionPriceModelResult Evaluate(Security security, Slice slice, OptionContract contract)
109  {
110  if (!AllowedOptionStyles.Contains(contract.Symbol.ID.OptionStyle))
111  {
112  throw new ArgumentException($"{contract.Symbol.ID.OptionStyle} style options are not supported by option price model '{this.GetType().Name}'");
113  }
114 
115  try
116  {
117  // expired options have no price
118  if (contract.Time.Date > contract.Expiry.Date)
119  {
120  if (Log.DebuggingEnabled)
121  {
122  Log.Debug($"QLOptionPriceModel.Evaluate(). Expired {contract.Symbol}. Time > Expiry: {contract.Time.Date} > {contract.Expiry.Date}");
123  }
125  }
126 
127  var dayCounter = new Actual365Fixed();
128  var securityExchangeHours = security.Exchange.Hours;
129  var maturityDate = AddDays(contract.Expiry.Date, Option.DefaultSettlementDays, securityExchangeHours);
130 
131  // Get time until maturity (in year)
132  var maturity = dayCounter.yearFraction(contract.Time.Date, maturityDate);
133  if (maturity < 0)
134  {
135  if (Log.DebuggingEnabled)
136  {
137  Log.Debug($"QLOptionPriceModel.Evaluate(). negative time ({maturity}) given for {contract.Symbol}. Time: {contract.Time.Date}. Maturity {maturityDate}");
138  }
140  }
141 
142  // setting up option pricing parameters
143  var optionSecurity = (Option)security;
144  var premium = (double)optionSecurity.Price;
145  var spot = (double)optionSecurity.Underlying.Price;
146 
147  if (spot <= 0d || premium <= 0d)
148  {
149  if (Log.DebuggingEnabled)
150  {
151  Log.Debug($"QLOptionPriceModel.Evaluate(). Non-positive prices for {contract.Symbol}. Premium: {premium}. Underlying price {spot}");
152  }
153 
155  }
156 
157  var calendar = new UnitedStates();
158  var settlementDate = AddDays(contract.Time.Date, Option.DefaultSettlementDays, securityExchangeHours);
159  var underlyingQuoteValue = new SimpleQuote(spot);
160 
161  var dividendYieldValue = new SimpleQuote(_dividendYieldEstimator.Estimate(security, slice, contract));
162  var dividendYield = new Handle<YieldTermStructure>(new FlatForward(0, calendar, dividendYieldValue, dayCounter));
163 
164  var riskFreeRateValue = new SimpleQuote((double)_riskFreeRateEstimator.Estimate(security, slice, contract));
165  var riskFreeRate = new Handle<YieldTermStructure>(new FlatForward(0, calendar, riskFreeRateValue, dayCounter));
166 
167  // Get discount factor by dividend and risk free rate using the maturity
168  var dividendDiscount = dividendYield.link.discount(maturity);
169  var riskFreeDiscount = riskFreeRate.link.discount(maturity);
170  var forwardPrice = spot * dividendDiscount / riskFreeDiscount;
171 
172  // Initial guess for volatility by Brenner and Subrahmanyam (1988)
173  var initialGuess = Math.Sqrt(2 * Math.PI / maturity) * premium / spot;
174 
175  var underlyingVolEstimate = _underlyingVolEstimator.Estimate(security, slice, contract);
176 
177  // If the volatility estimator is not ready, we will use initial guess
178  if (!_underlyingVolEstimator.IsReady)
179  {
180  underlyingVolEstimate = initialGuess;
181  }
182 
183  var underlyingVolValue = new SimpleQuote(underlyingVolEstimate);
184  var underlyingVol = new Handle<BlackVolTermStructure>(new BlackConstantVol(0, calendar, new Handle<Quote>(underlyingVolValue), dayCounter));
185 
186  // preparing stochastic process and payoff functions
187  var stochasticProcess = new BlackScholesMertonProcess(new Handle<Quote>(underlyingQuoteValue), dividendYield, riskFreeRate, underlyingVol);
188  var payoff = new PlainVanillaPayoff(contract.Right == OptionRight.Call ? QLNet.Option.Type.Call : QLNet.Option.Type.Put, (double)contract.Strike);
189 
190  // creating option QL object
191  var option = contract.Symbol.ID.OptionStyle == OptionStyle.American ?
192  new VanillaOption(payoff, new AmericanExercise(settlementDate, maturityDate)) :
193  new VanillaOption(payoff, new EuropeanExercise(maturityDate));
194 
195  // preparing pricing engine QL object
196  option.setPricingEngine(_pricingEngineFunc(contract.Symbol, stochasticProcess));
197 
198  // Setting the evaluation date before running the calculations
199  var evaluationDate = contract.Time.Date;
200  SetEvaluationDate(evaluationDate);
201 
202  // running calculations
203  var npv = EvaluateOption(option);
204 
205  BlackCalculator blackCalculator = null;
206 
207  // Calculate the Implied Volatility
208  var impliedVol = 0d;
209  try
210  {
211  SetEvaluationDate(evaluationDate);
212  impliedVol = option.impliedVolatility(premium, stochasticProcess);
213  }
214  catch (Exception e)
215  {
216  // A Newton-Raphson optimization estimate of the implied volatility
217  impliedVol = ImpliedVolatilityEstimation(premium, initialGuess, maturity, riskFreeDiscount, forwardPrice, payoff, out blackCalculator);
218  if (Log.DebuggingEnabled)
219  {
220  var referenceDate = underlyingVol.link.referenceDate();
221  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}");
222  }
223  }
224 
225  // Update the Black Vol Term Structure with the Implied Volatility to improve Greek calculation
226  // We assume that the underlying volatility model does not yield a good estimate and
227  // other sources, e.g. Interactive Brokers, use the implied volatility to calculate the Greeks
228  // After this operation, the Theoretical Price (NPV) will match the Premium, so we do not re-evalute
229  // it and let users compare NPV and the Premium if they wish.
230  underlyingVolValue.setValue(impliedVol);
231 
232  // 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
233  decimal tryGetGreekOrReevaluate(Func<double> greek, Func<BlackCalculator, double> black)
234  {
235  double result;
236  var isApproximation = false;
237  Exception exception = null;
238 
239  try
240  {
241  SetEvaluationDate(evaluationDate);
242  result = greek();
243  }
244  catch (Exception err)
245  {
246  exception = err;
247 
249  {
250  return 0.0m;
251  }
252 
253  if (blackCalculator == null)
254  {
255  // Define Black Calculator to calculate Greeks that are not defined by the option object
256  // Some models do not evaluate all greeks under some circumstances (e.g. low dividend yield)
257  // We override this restriction to calculate the Greeks directly with the BlackCalculator
258  var vol = underlyingVol.link.blackVol(maturityDate, (double)contract.Strike);
259  blackCalculator = CreateBlackCalculator(forwardPrice, riskFreeDiscount, vol, payoff);
260  }
261 
262  isApproximation = true;
263  result = black(blackCalculator);
264  }
265 
266  if (result.IsNaNOrInfinity())
267  {
268  if (Log.DebuggingEnabled)
269  {
270  var referenceDate = underlyingVol.link.referenceDate();
271  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}");
272  }
273 
274  return 0m;
275  }
276 
277  var value = result.SafeDecimalCast();
278 
279  if (value == decimal.Zero && Log.DebuggingEnabled)
280  {
281  var referenceDate = underlyingVol.link.referenceDate();
282  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}");
283  return value;
284  }
285 
286  return value;
287  }
288 
289  // producing output with lazy calculations of greeks
290  return new OptionPriceModelResult(npv, // EvaluateOption ensure it is not NaN or Infinity
291  () => impliedVol.IsNaNOrInfinity() ? 0m : impliedVol.SafeDecimalCast(),
292  () => new Greeks(() => tryGetGreekOrReevaluate(() => option.delta(), (black) => black.delta(spot)),
293  () => tryGetGreekOrReevaluate(() => option.gamma(), (black) => black.gamma(spot)),
294  () => tryGetGreekOrReevaluate(() => option.vega(), (black) => black.vega(maturity)) / 100, // per cent
295  () => tryGetGreekOrReevaluate(() => option.theta(), (black) => black.theta(spot, maturity)),
296  () => tryGetGreekOrReevaluate(() => option.rho(), (black) => black.rho(maturity)) / 100, // per cent
297  () => tryGetGreekOrReevaluate(() => option.elasticity(), (black) => black.elasticity(spot))));
298  }
299  catch (Exception err)
300  {
301  Log.Debug($"QLOptionPriceModel.Evaluate() error: {err.Message} {(Log.DebuggingEnabled ? err.StackTrace : string.Empty)} for {contract.Symbol}");
303  }
304  }
305 
306  /// <summary>
307  /// Runs option evaluation and logs exceptions
308  /// </summary>
309  /// <param name="option"></param>
310  /// <returns></returns>
311  private static decimal EvaluateOption(VanillaOption option)
312  {
313  try
314  {
315  var npv = option.NPV();
316 
317  if (double.IsNaN(npv) ||
318  double.IsInfinity(npv))
319  return 0;
320 
321  // can return negative value in neighborhood of 0
322  return Math.Max(0, npv).SafeDecimalCast();
323  }
324  catch (Exception err)
325  {
326  Log.Debug($"QLOptionPriceModel.EvaluateOption() error: {err.Message}");
327  return 0;
328  }
329  }
330 
331  /// <summary>
332  /// An implied volatility approximation by Newton-Raphson method. Return 0 if result is not converged
333  /// </summary>
334  /// <remarks>
335  /// Orlando G, Taglialatela G. A review on implied volatility calculation. Journal of Computational and Applied Mathematics. 2017 Aug 15;320:202-20.
336  /// https://www.sciencedirect.com/science/article/pii/S0377042717300602
337  /// </remarks>
338  /// <param name="price">current price of the option</param>
339  /// <param name="initialGuess">initial guess of the IV</param>
340  /// <param name="timeTillExpiry">time till option contract expiry</param>
341  /// <param name="riskFreeDiscount">risk free rate discount factor</param>
342  /// <param name="forwardPrice">future value of underlying price</param>
343  /// <param name="payoff">payoff structure of the option contract</param>
344  /// <param name="black">black calculator instance</param>
345  /// <returns>implied volatility estimation</returns>
346  protected double ImpliedVolatilityEstimation(double price, double initialGuess, double timeTillExpiry, double riskFreeDiscount,
347  double forwardPrice, PlainVanillaPayoff payoff, out BlackCalculator black)
348  {
349  // Set up the optimizer
350  const double tolerance = 1e-3d;
351  const double lowerBound = 1e-7d;
352  const double upperBound = 4d;
353  var iterRemain = 10;
354  var error = double.MaxValue;
355  var impliedVolEstimate = initialGuess;
356 
357  // Set up option calculator
358  black = CreateBlackCalculator(forwardPrice, riskFreeDiscount, initialGuess, payoff);
359 
360  while (error > tolerance && iterRemain > 0)
361  {
362  var oldImpliedVol = impliedVolEstimate;
363 
364  // Set up calculator by previous IV estimate to get new theoretical price, vega and IV
365  black = CreateBlackCalculator(forwardPrice, riskFreeDiscount, oldImpliedVol, payoff);
366  impliedVolEstimate -= (black.value() - price) / black.vega(timeTillExpiry);
367 
368  if (impliedVolEstimate < lowerBound)
369  {
370  impliedVolEstimate = lowerBound;
371  }
372  else if (impliedVolEstimate > upperBound)
373  {
374  impliedVolEstimate = upperBound;
375  }
376 
377  error = Math.Abs(impliedVolEstimate - oldImpliedVol) / impliedVolEstimate;
378  iterRemain--;
379  }
380 
381  if (iterRemain == 0)
382  {
383  if (Log.DebuggingEnabled)
384  {
385  Log.Debug("QLOptionPriceModel.ImpliedVolatilityEstimation() error: Implied Volatility approxiation did not converge, returning 0.");
386  }
387  return 0d;
388  }
389 
390  return impliedVolEstimate;
391  }
392 
393  /// <summary>
394  /// Define Black Calculator to calculate Greeks that are not defined by the option object
395  /// Some models do not evaluate all greeks under some circumstances (e.g. low dividend yield)
396  /// We override this restriction to calculate the Greeks directly with the BlackCalculator
397  /// </summary>
398  private BlackCalculator CreateBlackCalculator(double forwardPrice, double riskFreeDiscount, double stdDev, PlainVanillaPayoff payoff)
399  {
400  return new BlackCalculator(payoff, forwardPrice, stdDev, riskFreeDiscount);
401  }
402 
403  private static DateTime AddDays(DateTime date, int days, SecurityExchangeHours marketHours)
404  {
405  var forwardDate = date.AddDays(days);
406 
407  if (!marketHours.IsDateOpen(forwardDate))
408  {
409  forwardDate = marketHours.GetNextTradingDay(forwardDate);
410  }
411 
412  return forwardDate;
413  }
414 
415  /// <summary>
416  /// Set the evaluation date
417  /// </summary>
418  /// <param name="evaluationDate">The current evaluation date</param>
419  private void SetEvaluationDate(DateTime evaluationDate)
420  {
421  if (Settings.evaluationDate().ToDateTime() != evaluationDate)
422  {
423  Settings.setEvaluationDate(evaluationDate);
424  }
425  }
426  }
427 }