Skip to content

Commit

Permalink
Address 2nd review
Browse files Browse the repository at this point in the history
  • Loading branch information
LouisSzeto committed Jan 12, 2024
1 parent a7d5b9c commit 35d9b36
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 184 deletions.
8 changes: 6 additions & 2 deletions Algorithm/QCAlgorithm.Indicators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -858,10 +858,14 @@ public Identity Identity(Symbol symbol, TimeSpan resolution, Func<IBaseData, dec
/// <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 = 0.05m, int period = 252, OptionPricingModelType optionModel = OptionPricingModelType.BlackScholes, Resolution? resolution = null)
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);
var iv = new ImpliedVolatility(name, symbol, riskFreeRate, period, optionModel);
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;
Expand Down
136 changes: 100 additions & 36 deletions Indicators/ImpliedVolatility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@

using System;
using MathNet.Numerics.RootFinding;
using Python.Runtime;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Market;
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 : BarIndicator, IIndicatorWarmUpPeriodProvider
public class ImpliedVolatility : IndicatorBase<IndicatorDataPoint>, IIndicatorWarmUpPeriodProvider
{
private readonly Symbol _optionSymbol;
private readonly Symbol _underlyingSymbol;
Expand All @@ -34,29 +36,34 @@ public class ImpliedVolatility : BarIndicator, IIndicatorWarmUpPeriodProvider
private OptionPricingModelType _optionModel;

/// <summary>
/// Gets the expiration time of the option
/// Risk-free rate model
/// </summary>
public DateTime Expiry { get; }
private readonly IRiskFreeInterestRateModel _riskFreeInterestRateModel;

/// <summary>
/// Gets the option right (call/put) of the option
/// Gets the expiration time of the option
/// </summary>
public OptionRight Right { get; }
public DateTime Expiry => _optionSymbol.ID.Date;

/// <summary>
/// Risk Free Rate
/// Gets the option right (call/put) of the option
/// </summary>
public decimal RiskFreeRate { get; set; }
public OptionRight Right => _optionSymbol.ID.OptionRight;

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

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

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

/// <summary>
/// Gets the historical volatility of the underlying
Expand All @@ -73,28 +80,15 @@ public class ImpliedVolatility : BarIndicator, IIndicatorWarmUpPeriodProvider
/// </summary>
public IndicatorBase<IndicatorDataPoint> UnderlyingPrice { get; }

/// <summary>
/// Initializes a new instance of the ImpliedVolatility class
/// </summary>
/// <param name="option">The option to be tracked</param>am>
/// <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>
public ImpliedVolatility(Symbol option, decimal riskFreeRate = 0.05m, int period = 252,
OptionPricingModelType optionModel = OptionPricingModelType.BlackScholes)
: this($"IV({option.Value},{riskFreeRate},{period},{optionModel})", option, riskFreeRate, 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">The risk free rate</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, decimal riskFreeRate = 0.05m, int period = 252,
public ImpliedVolatility(string name, Symbol option, IRiskFreeInterestRateModel riskFreeRateModel, int period = 252,
OptionPricingModelType optionModel = OptionPricingModelType.BlackScholes)
: base(name)
{
Expand All @@ -107,14 +101,10 @@ public ImpliedVolatility(string name, Symbol option, decimal riskFreeRate = 0.05
_optionSymbol = option;
_underlyingSymbol = option.Underlying;
_roc = new(1);
_riskFreeInterestRateModel = riskFreeRateModel;
_optionModel = optionModel;

Strike = sid.StrikePrice;
Expiry = sid.Date;
Right = sid.OptionRight;
Style = sid.OptionStyle;
RiskFreeRate = riskFreeRate;

RiskFreeRate = new Identity(name + "_RiskFreeRate");
HistoricalVolatility = IndicatorExtensions.Times(
IndicatorExtensions.Of(
new StandardDeviation(period),
Expand All @@ -133,6 +123,75 @@ public ImpliedVolatility(string name, Symbol option, decimal riskFreeRate = 0.05
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>
Expand All @@ -148,17 +207,19 @@ public ImpliedVolatility(string name, Symbol option, decimal riskFreeRate = 0.05
/// </summary>
/// <param name="input">The input given to the indicator</param>
/// <returns>The input is returned unmodified.</returns>
protected override decimal ComputeNextValue(IBaseDataBar input)
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.Close);
Price.Update(input.EndTime, input.Price);
}
else if (inputSymbol == _underlyingSymbol)
{
_consolidator.Update(input);
UnderlyingPrice.Update(input.EndTime, input.Close);
UnderlyingPrice.Update(input.EndTime, input.Price);
}
else
{
Expand Down Expand Up @@ -194,14 +255,15 @@ private decimal CalculateIV(DateTime time)
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, Right, _optionModel) - price);
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, 0.01d, 1.0d, 1e-5d, 20));
}
catch
{
Log.Error("Fail to converge, returning 0.");
Log.Error("ImpliedVolatility.CalculateIV(): Fail to converge, returning 0.");
return 0m;
}
}
Expand All @@ -211,12 +273,14 @@ private decimal CalculateIV(DateTime time)
/// </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();
Expand Down
2 changes: 2 additions & 0 deletions Indicators/OptionPricingModelType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ public enum OptionPricingModelType
/// <summary>
/// Vanilla Black Scholes Model
/// </summary>
/// <remarks>Preferred on calculating greeks for European options, and IV for all options</remarks>
BlackScholes,
/// <summary>
/// The Cox-Ross-Rubinstein binomial tree model (CRR model)
/// </summary>
/// <remarks>Preferred on calculating greeks for American options</remarks>
BinomialCoxRossRubinstein
}
}
36 changes: 31 additions & 5 deletions Tests/Algorithm/AlgorithmIndicatorsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
*/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;

using Moq;
using NUnit.Framework;
Expand All @@ -26,7 +22,6 @@
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Indicators;
using QuantConnect.Interfaces;
using QuantConnect.Lean.Engine.DataFeeds;
using QuantConnect.Lean.Engine.HistoricalData;
using QuantConnect.Tests.Engine.DataFeeds;
Expand All @@ -37,6 +32,7 @@ namespace QuantConnect.Tests.Algorithm
public class AlgorithmIndicatorsTests
{
private QCAlgorithm _algorithm;
private Symbol _option;

[SetUp]
public void Setup()
Expand All @@ -52,6 +48,7 @@ public void Setup()

_algorithm.SetDateTime(new DateTime(2013, 10, 11, 15, 0, 0));
_algorithm.AddEquity("SPY");
_option = _algorithm.AddOption("SPY").Symbol;
_algorithm.EnableAutomaticIndicatorWarmUp = true;
}

Expand Down Expand Up @@ -93,5 +90,34 @@ public void SharpeRatioIndicatorUsesAlgorithmsRiskFreeRateModelSetAfterIndicator
// Our interest rate provider should have been called once
interestRateProviderMock.Verify(x => x.GetInterestRate(reference), Times.Once);
}

[Test]
public void IVIndicatorUsesAlgorithmsRiskFreeRateModelSetAfterIndicatorRegistration()
{
// Register indicator
var sharpeRatio = _algorithm.IV(_option);

// Setup risk free rate model
var interestRateProviderMock = new Mock<IRiskFreeInterestRateModel>();
var reference = new DateTime(2023, 11, 21, 10, 0, 0);
interestRateProviderMock.Setup(x => x.GetInterestRate(reference)).Verifiable();

// Update indicator
sharpeRatio.Update(new IndicatorDataPoint(_option, reference, 30m));
sharpeRatio.Update(new IndicatorDataPoint(Symbols.SPY, reference, 300m));

// Our interest rate provider shouldn't have been called yet since it's hasn't been set to the algorithm
interestRateProviderMock.Verify(x => x.GetInterestRate(reference), Times.Never);

// Set the interest rate provider to the algorithm
_algorithm.SetRiskFreeInterestRateModel(interestRateProviderMock.Object);

// Update indicator
sharpeRatio.Update(new IndicatorDataPoint(_option, reference, 30m));
sharpeRatio.Update(new IndicatorDataPoint(Symbols.SPY, reference, 300m));

// Our interest rate provider should have been called once by each update
interestRateProviderMock.Verify(x => x.GetInterestRate(reference), Times.Exactly(2));
}
}
}
Loading

0 comments on commit 35d9b36

Please sign in to comment.