/* * 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 System.Runtime.CompilerServices; using MathNet.Numerics.Distributions; using QuantConnect.Util; namespace QuantConnect.Indicators { /// /// Helper class for option greeks related indicators /// public class OptionGreekIndicatorsHelper { /// /// Number of steps in binomial tree simulation to obtain Greeks/IV /// public const int Steps = 200; /// /// Returns the Black theoretical price for the given arguments /// public static double BlackTheoreticalPrice(double volatility, double spotPrice, double strikePrice, double timeToExpiration, double riskFreeRate, double dividendYield, OptionRight optionType) { var d1 = CalculateD1(spotPrice, strikePrice, timeToExpiration, riskFreeRate, dividendYield, volatility); var d2 = CalculateD2(d1, volatility, timeToExpiration); var norm = new Normal(); var optionPrice = 0.0; if (optionType == OptionRight.Call) { optionPrice = spotPrice * Math.Exp(-dividendYield * timeToExpiration) * norm.CumulativeDistribution(d1) - strikePrice * Math.Exp(-riskFreeRate * timeToExpiration) * norm.CumulativeDistribution(d2); } else if (optionType == OptionRight.Put) { optionPrice = strikePrice * Math.Exp(-riskFreeRate * timeToExpiration) * norm.CumulativeDistribution(-d2) - spotPrice * Math.Exp(-dividendYield * timeToExpiration) * norm.CumulativeDistribution(-d1); } else { throw new ArgumentException("Invalid option right."); } return optionPrice; } internal static double CalculateD1(double spotPrice, double strikePrice, double timeToExpiration, double riskFreeRate, double dividendYield, double volatility) { var numerator = Math.Log(spotPrice / strikePrice) + (riskFreeRate - dividendYield + 0.5 * volatility * volatility) * timeToExpiration; var denominator = volatility * Math.Sqrt(Math.Max(0, timeToExpiration)); if (denominator == 0) { // return a random variable large enough to produce normal probability density close to 1 return 10; } return numerator / denominator; } internal static double CalculateD2(double d1, double volatility, double timeToExpiration) { return d1 - volatility * Math.Sqrt(Math.Max(0, timeToExpiration)); } /// /// Creates a Binomial Theoretical Price Tree from the given parameters /// /// Reference: https://en.wikipedia.org/wiki/Binomial_options_pricing_model#Step_1:_Create_the_binomial_price_tree public static double CRRTheoreticalPrice(double volatility, double spotPrice, double strikePrice, double timeToExpiration, double riskFreeRate, double dividendYield, OptionRight optionType, int steps = Steps) { var deltaTime = timeToExpiration / steps; var upFactor = Math.Exp(volatility * Math.Sqrt(deltaTime)); if (upFactor == 1) { // Introduce a very small factor to avoid constant tree while staying low volatility upFactor = 1.0001; } var downFactor = 1 / upFactor; var probUp = (Math.Exp((riskFreeRate - dividendYield) * deltaTime) - downFactor) / (upFactor - downFactor); return BinomialTheoreticalPrice(deltaTime, probUp, upFactor, riskFreeRate, spotPrice, strikePrice, optionType, steps); } /// /// Creates the Forward Binomial Theoretical Price Tree from the given parameters /// public static double ForwardTreeTheoreticalPrice(double volatility, double spotPrice, double strikePrice, double timeToExpiration, double riskFreeRate, double dividendYield, OptionRight optionType, int steps = Steps) { var deltaTime = timeToExpiration / steps; var discount = Math.Exp((riskFreeRate - dividendYield) * deltaTime); var volatilityTimeSqrtDeltaTime = volatility * Math.Sqrt(deltaTime); var upFactor = Math.Exp(volatilityTimeSqrtDeltaTime) * discount; var downFactor = Math.Exp(-volatilityTimeSqrtDeltaTime) * discount; if (upFactor - downFactor == 0) { // Introduce a very small factor // to avoid constant tree while staying low volatility upFactor = 1.0001; downFactor = 0.9999; } var probUp = (discount - downFactor) / (upFactor - downFactor); return BinomialTheoreticalPrice(deltaTime, probUp, upFactor, riskFreeRate, spotPrice, strikePrice, optionType, steps); } private static double BinomialTheoreticalPrice(double deltaTime, double probUp, double upFactor, double riskFreeRate, double spotPrice, double strikePrice, OptionRight optionType, int steps = Steps) { var probDown = 1 - probUp; var values = new double[steps + 1]; // Cache for exercise values for Call options to avoid recalculating them var exerciseValues = optionType == OptionRight.Call ? new double[2 * steps] : null; for (int i = 0; i < (exerciseValues?.Length ?? values.Length); i++) { if (i < values.Length) { var nextPrice = spotPrice * Math.Pow(upFactor, 2 * i - steps); values[i] = OptionPayoff.GetIntrinsicValue(nextPrice, strikePrice, optionType); } if (optionType == OptionRight.Call) { var nextPrice = spotPrice * Math.Pow(upFactor, i - steps); exerciseValues[i] = OptionPayoff.GetIntrinsicValue(nextPrice, strikePrice, optionType); } } var factor = Math.Exp(-riskFreeRate * deltaTime); var factorA = factor * probDown; var factorB = factor * probUp; for (var period = steps - 1; period >= 0; period--) { for (var i = 0; i <= period; i++) { var binomialValue = values[i] * factorA + values[i + 1] * factorB; // No advantage for American put option to exercise early in risk-neutral setting if (optionType == OptionRight.Put) { values[i] = binomialValue; continue; } values[i] = Math.Max(binomialValue, exerciseValues[2 * i - period + steps]); } } return values[0]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double TimeTillExpiry(DateTime expiry, DateTime referenceDate) { return (expiry - referenceDate).TotalDays / 365d; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static double Divide(double numerator, double denominator) { if (denominator != 0) { return numerator / denominator; } //Log.Error("OptionGreekIndicatorsHelper.Divide(): Division by zero detected. Returning 0."); return 0; } } }