/*
* 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;
}
}
}