Skip to content

Commit

Permalink
Implied Volatility Indicator for Options (#7680)
Browse files Browse the repository at this point in the history
* Add IV indicator

* Fix bug

* Add QCAlgorithm helper method

* Add unit tests

* Add OptionPricingModelType, as option for greeks & IV estimation

* Nit, and fix bug

* Address peer review

* Fix bug

* Fix bug on CRR

* Ensure test indicator initiate correctly

* Address 2nd review

* Check for FOPs and index options as well

* Add comparison with QuantLib

* CRR not follow BSM

* minor bug fix
  • Loading branch information
LouisSzeto authored Jan 18, 2024
1 parent 8ec2c4b commit 0b833b0
Show file tree
Hide file tree
Showing 8 changed files with 868 additions and 7 deletions.
24 changes: 24 additions & 0 deletions Algorithm/QCAlgorithm.Indicators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,30 @@ public Identity Identity(Symbol symbol, TimeSpan resolution, Func<IBaseData, dec
return identity;
}

/// <summary>
/// Creates a new ImpliedVolatility indicator for the symbol The indicator will be automatically
/// updated on the symbol's subscription resolution
/// </summary>
/// <param name="symbol">The option symbol whose values we want as an indicator</param>
/// <param name="riskFreeRate">The risk free rate</param>
/// <param name="period">The lookback period of historical volatility</param>
/// <param name="optionModel">The option pricing model used to estimate IV</param>
/// <param name="resolution">The desired resolution of the data</param>
/// <returns>A new ImpliedVolatility indicator for the specified symbol</returns>
[DocumentationAttribute(Indicators)]
public ImpliedVolatility IV(Symbol symbol, decimal? riskFreeRate = null, int period = 252, OptionPricingModelType optionModel = OptionPricingModelType.BlackScholes, Resolution? resolution = null)
{
var name = CreateIndicatorName(symbol, $"IV({riskFreeRate},{period},{optionModel})", resolution);
IRiskFreeInterestRateModel riskFreeRateModel = riskFreeRate.HasValue
? new ConstantRiskFreeRateInterestRateModel(riskFreeRate.Value)
// Make it a function so it's lazily evaluated: SetRiskFreeInterestRateModel can be called after this method
: new FuncRiskFreeRateInterestRateModel((datetime) => RiskFreeInterestRateModel.GetInterestRate(datetime));
var iv = new ImpliedVolatility(name, symbol, riskFreeRateModel, period, optionModel);
RegisterIndicator(symbol, iv, ResolveConsolidator(symbol, resolution));
RegisterIndicator(symbol.Underlying, iv, ResolveConsolidator(symbol, resolution));
return iv;
}

/// <summary>
/// Creates a new KaufmanAdaptiveMovingAverage indicator.
/// </summary>
Expand Down
5 changes: 3 additions & 2 deletions Common/Securities/Option/Option.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using QuantConnect.Orders.Slippage;
using QuantConnect.Python;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Util;
using System;
using System.Collections.Generic;

Expand Down Expand Up @@ -328,7 +329,7 @@ public bool IsAutoExercised(decimal underlyingPrice)
/// </summary>
public decimal GetIntrinsicValue(decimal underlyingPrice)
{
return Math.Max(0.0m, GetPayOff(underlyingPrice));
return OptionPayoff.GetIntrinsicValue(underlyingPrice, StrikePrice, Right);
}

/// <summary>
Expand All @@ -338,7 +339,7 @@ public decimal GetIntrinsicValue(decimal underlyingPrice)
/// <returns></returns>
public decimal GetPayOff(decimal underlyingPrice)
{
return Right == OptionRight.Call ? underlyingPrice - StrikePrice : StrikePrice - underlyingPrice;
return OptionPayoff.GetPayOff(underlyingPrice, StrikePrice, Right);
}

/// <summary>
Expand Down
46 changes: 46 additions & 0 deletions Common/Util/OptionPayoff.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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;

namespace QuantConnect.Util
{
public static class OptionPayoff
{
/// <summary>
/// Intrinsic value function of the option
/// </summary>
/// <param name="underlyingPrice">The price of the underlying</param>
/// <param name="strike">The strike price of the option</param>
/// <param name="right">The option right of the option, call or put</param>
/// <returns>The intrinsic value remains for the option at expiry</returns>
public static decimal GetIntrinsicValue(decimal underlyingPrice, decimal strike, OptionRight right)
{
return Math.Max(0.0m, GetPayOff(underlyingPrice, strike, right));
}

/// <summary>
/// Option payoff function at expiration time
/// </summary>
/// <param name="underlyingPrice">The price of the underlying</param>
/// <param name="strike">The strike price of the option</param>
/// <param name="right">The option right of the option, call or put</param>
/// <returns></returns>
public static decimal GetPayOff(decimal underlyingPrice, decimal strike, OptionRight right)
{
return right == OptionRight.Call ? underlyingPrice - strike : strike - underlyingPrice;
}
}
}
291 changes: 291 additions & 0 deletions Indicators/ImpliedVolatility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
/*
* 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.Data.Consolidators;
using QuantConnect.Logging;
using QuantConnect.Python;

namespace QuantConnect.Indicators
{
/// <summary>
/// Implied Volatility indicator that calculate the IV of an option using Black-Scholes Model
/// </summary>
public class ImpliedVolatility : IndicatorBase<IndicatorDataPoint>, IIndicatorWarmUpPeriodProvider
{
private readonly Symbol _optionSymbol;
private readonly Symbol _underlyingSymbol;
private BaseDataConsolidator _consolidator;
private RateOfChange _roc;
private decimal _impliedVolatility;
private OptionPricingModelType _optionModel;

/// <summary>
/// Risk-free rate model
/// </summary>
private readonly IRiskFreeInterestRateModel _riskFreeInterestRateModel;

/// <summary>
/// Gets the expiration time of the option
/// </summary>
public DateTime Expiry => _optionSymbol.ID.Date;

/// <summary>
/// Gets the option right (call/put) of the option
/// </summary>
public OptionRight Right => _optionSymbol.ID.OptionRight;

/// <summary>
/// Gets the strike price of the option
/// </summary>
public decimal Strike => _optionSymbol.ID.StrikePrice;

/// <summary>
/// Gets the option style (European/American) of the option
/// </summary>
public OptionStyle Style => _optionSymbol.ID.OptionStyle;

/// <summary>
/// Risk Free Rate
/// </summary>
public Identity RiskFreeRate { get; set; }

/// <summary>
/// Gets the historical volatility of the underlying
/// </summary>
public IndicatorBase<IndicatorDataPoint> HistoricalVolatility { get; }

/// <summary>
/// Gets the option price level
/// </summary>
public IndicatorBase<IndicatorDataPoint> Price { get; }

/// <summary>
/// Gets the underlying's price level
/// </summary>
public IndicatorBase<IndicatorDataPoint> UnderlyingPrice { get; }

/// <summary>
/// Initializes a new instance of the ImpliedVolatility class
/// </summary>
/// <param name="name">The name of this indicator</param>
/// <param name="option">The option to be tracked</param>
/// <param name="riskFreeRateModel">Risk-free rate model</param>
/// <param name="period">The lookback period of historical volatility</param>
/// <param name="optionModel">The option pricing model used to estimate IV</param>
public ImpliedVolatility(string name, Symbol option, IRiskFreeInterestRateModel riskFreeRateModel, int period = 252,
OptionPricingModelType optionModel = OptionPricingModelType.BlackScholes)
: base(name)
{
var sid = option.ID;
if (!sid.SecurityType.IsOption())
{
throw new ArgumentException("ImpliedVolatility only support SecurityType.Option.");
}

_optionSymbol = option;
_underlyingSymbol = option.Underlying;
_roc = new(1);
_riskFreeInterestRateModel = riskFreeRateModel;
_optionModel = optionModel;

RiskFreeRate = new Identity(name + "_RiskFreeRate");
HistoricalVolatility = IndicatorExtensions.Times(
IndicatorExtensions.Of(
new StandardDeviation(period),
_roc
),
Convert.ToDecimal(Math.Sqrt(252))
);
Price = new Identity(name + "_Close");
UnderlyingPrice = new Identity(name + "_UnderlyingClose");

_consolidator = new(TimeSpan.FromDays(1));
_consolidator.DataConsolidated += (_, bar) => {
_roc.Update(bar.EndTime, bar.Price);
};

WarmUpPeriod = period;
}

/// <summary>
/// Initializes a new instance of the ImpliedVolatility class
/// </summary>
/// <param name="option">The option to be tracked</param>
/// <param name="riskFreeRateModel">Risk-free rate model</param>
/// <param name="period">The lookback period of historical volatility</param>
/// <param name="optionModel">The option pricing model used to estimate IV</param>
public ImpliedVolatility(Symbol option, IRiskFreeInterestRateModel riskFreeRateModel, int period = 252,
OptionPricingModelType optionModel = OptionPricingModelType.BlackScholes)
: this($"IV({option.Value},{period},{optionModel})", option, riskFreeRateModel, period, optionModel)
{
}

/// <summary>
/// Initializes a new instance of the ImpliedVolatility class
/// </summary>
/// <param name="name">The name of this indicator</param>
/// <param name="option">The option to be tracked</param>
/// <param name="riskFreeRateModel">Risk-free rate model</param>
/// <param name="period">The lookback period of historical volatility</param>
/// <param name="optionModel">The option pricing model used to estimate IV</param>
public ImpliedVolatility(string name, Symbol option, PyObject riskFreeRateModel, int period = 252,
OptionPricingModelType optionModel = OptionPricingModelType.BlackScholes)
: this(name, option, RiskFreeInterestRateModelPythonWrapper.FromPyObject(riskFreeRateModel), period, optionModel)
{
}

/// <summary>
/// Initializes a new instance of the ImpliedVolatility class
/// </summary>
/// <param name="option">The option to be tracked</param>
/// <param name="riskFreeRateModel">Risk-free rate model</param>
/// <param name="period">The lookback period of historical volatility</param>
/// <param name="optionModel">The option pricing model used to estimate IV</param>
public ImpliedVolatility(Symbol option, PyObject riskFreeRateModel, int period = 252,
OptionPricingModelType optionModel = OptionPricingModelType.BlackScholes)
: this($"IV({option.Value},{period},{optionModel})", option,
RiskFreeInterestRateModelPythonWrapper.FromPyObject(riskFreeRateModel), period, optionModel)
{
}

/// <summary>
/// Initializes a new instance of the ImpliedVolatility class
/// </summary>
/// <param name="name">The name of this indicator</param>
/// <param name="option">The option to be tracked</param>
/// <param name="riskFreeRate">Risk-free rate, as a constant</param>
/// <param name="period">The lookback period of historical volatility</param>
/// <param name="optionModel">The option pricing model used to estimate IV</param>
public ImpliedVolatility(string name, Symbol option, decimal riskFreeRate = 0.05m, int period = 252,
OptionPricingModelType optionModel = OptionPricingModelType.BlackScholes)
: this(name, option, new ConstantRiskFreeRateInterestRateModel(riskFreeRate), period, optionModel)
{
}

/// <summary>
/// Initializes a new instance of the ImpliedVolatility class
/// </summary>
/// <param name="option">The option to be tracked</param>
/// <param name="riskFreeRate">Risk-free rate, as a constant</param>
/// <param name="period">The lookback period of historical volatility</param>
/// <param name="optionModel">The option pricing model used to estimate IV</param>
public ImpliedVolatility(Symbol option, decimal riskFreeRate = 0.05m, int period = 252,
OptionPricingModelType optionModel = OptionPricingModelType.BlackScholes)
: this($"IV({option.Value},{period},{riskFreeRate},{optionModel})", option,
new ConstantRiskFreeRateInterestRateModel(riskFreeRate), period, optionModel)
{
}

/// <summary>
/// Gets a flag indicating when this indicator is ready and fully initialized
/// </summary>
public override bool IsReady => HistoricalVolatility.Samples >= 2 && Price.Current.Time == UnderlyingPrice.Current.Time;

/// <summary>
/// Required period, in data points, for the indicator to be ready and fully initialized.
/// </summary>
public int WarmUpPeriod { get; }

/// <summary>
/// Computes the next value
/// </summary>
/// <param name="input">The input given to the indicator</param>
/// <returns>The input is returned unmodified.</returns>
protected override decimal ComputeNextValue(IndicatorDataPoint input)
{
RiskFreeRate.Update(input.EndTime, _riskFreeInterestRateModel.GetInterestRate(input.EndTime));

var inputSymbol = input.Symbol;
if (inputSymbol == _optionSymbol)
{
Price.Update(input.EndTime, input.Price);
}
else if (inputSymbol == _underlyingSymbol)
{
_consolidator.Update(input);
UnderlyingPrice.Update(input.EndTime, input.Price);
}
else
{
throw new ArgumentException("The given symbol was not target or reference symbol");
}

var time = Price.Current.Time;
if (time == UnderlyingPrice.Current.Time && Price.IsReady && UnderlyingPrice.IsReady)
{
_impliedVolatility = CalculateIV(time);
}
return _impliedVolatility;
}

// Calculate the theoretical option price
private decimal TheoreticalPrice(decimal volatility, decimal spotPrice, decimal strikePrice, decimal timeToExpiration, decimal riskFreeRate,
OptionRight optionType, OptionPricingModelType optionModel = OptionPricingModelType.BlackScholes)
{
switch (optionModel)
{
// Binomial model also follows BSM process (log-normal)
case OptionPricingModelType.BinomialCoxRossRubinstein:
return OptionGreekIndicatorsHelper.CRRTheoreticalPrice(volatility, spotPrice, strikePrice, timeToExpiration, riskFreeRate, optionType);
case OptionPricingModelType.BlackScholes:
default:
return OptionGreekIndicatorsHelper.BlackTheoreticalPrice(volatility, spotPrice, strikePrice, timeToExpiration, riskFreeRate, optionType);
}
}

// Calculate the IV of the option
private decimal CalculateIV(DateTime time)
{
var price = Price.Current.Value;
var spotPrice = UnderlyingPrice.Current.Value;
var timeToExpiration = Convert.ToDecimal((Expiry - time).TotalDays) / 365m;

Func<double, double> f = (vol) =>
(double)(TheoreticalPrice(Convert.ToDecimal(vol), spotPrice, Strike, timeToExpiration, RiskFreeRate.Current.Value, Right, _optionModel) - price);
try
{
return Convert.ToDecimal(Brent.FindRoot(f, 1e-7d, 4.0d, 1e-4d, 100));
}
catch
{
Log.Error("ImpliedVolatility.CalculateIV(): Fail to converge, returning 0.");
return 0m;
}
}

/// <summary>
/// Resets this indicator and all sub-indicators
/// </summary>
public override void Reset()
{
_consolidator.Dispose();
_consolidator = new(TimeSpan.FromDays(1));
_consolidator.DataConsolidated += (_, bar) => {
_roc.Update(bar.EndTime, bar.Price);
};

_roc.Reset();
RiskFreeRate.Reset();
HistoricalVolatility.Reset();
Price.Reset();
UnderlyingPrice.Reset();
base.Reset();
}
}
}
Loading

0 comments on commit 0b833b0

Please sign in to comment.