Files
2026-07-13 13:02:50 +08:00

465 lines
19 KiB
C#

/*
* 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.Collections.Generic;
using System.Globalization;
using System.Linq;
using Moq;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Data;
using QuantConnect.Indicators;
using QuantConnect.Lean.Engine.DataFeeds;
using QuantConnect.Lean.Engine.HistoricalData;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Indicators
{
public abstract class OptionBaseIndicatorTests<T> : CommonIndicatorTests<IBaseData>
where T : OptionIndicatorBase
{
// count of risk free rate calls per each update on opiton indicator
protected int RiskFreeRateUpdatesPerIteration { get; set; }
// count of dividend yield calls per each update on option indicator
protected int DividendYieldUpdatesPerIteration { get; set; }
protected static DateTime _reference = new DateTime(2023, 8, 1, 10, 0, 0);
protected static Symbol _symbol = Symbol.CreateOption("SPY", Market.USA, OptionStyle.American, OptionRight.Call, 450m, new DateTime(2023, 9, 1));
protected Symbol _underlying => _symbol.Underlying;
protected override IndicatorBase<IBaseData> CreateIndicator()
{
throw new NotImplementedException("method `CreateIndicator()` is required to be set up");
}
protected virtual OptionIndicatorBase CreateIndicator(IRiskFreeInterestRateModel riskFreeRateModel)
{
throw new NotImplementedException("method `CreateIndicator(IRiskFreeInterestRateModel riskFreeRateModel)` is required to be set up");
}
protected virtual OptionIndicatorBase CreateIndicator(IRiskFreeInterestRateModel riskFreeRateModel, IDividendYieldModel dividendYieldModel)
{
throw new NotImplementedException("method `CreateIndicator(IRiskFreeInterestRateModel riskFreeRateModel, IDividendYieldModel dividendYieldModel)` is required to be set up");
}
protected virtual OptionIndicatorBase CreateIndicator(QCAlgorithm algorithm)
{
throw new NotImplementedException("method `CreateIndicator(QCAlgorithm algorithm)` is required to be set up");
}
protected OptionPricingModelType ParseSymbols(string[] items, bool american, out Symbol call, out Symbol put)
{
var ticker = items[0];
var expiry = DateTime.ParseExact(items[1], "dd/MM/yyyy HH:mm:ss", CultureInfo.InvariantCulture);
var strike = Parse.Decimal(items[2]);
var style = american ? OptionStyle.American : OptionStyle.European;
call = Symbol.CreateOption(ticker, Market.USA, style, OptionRight.Call, strike, expiry);
put = Symbol.CreateOption(ticker, Market.USA, style, OptionRight.Put, strike, expiry);
return american ? OptionPricingModelType.ForwardTree : OptionPricingModelType.BlackScholes;
}
protected void RunTestIndicator(Symbol call, Symbol put, OptionIndicatorBase callIndicator, OptionIndicatorBase putIndicator,
string[] items, int callColumn, int putColumn, double errorRate, double errorMargin = 1e-4)
{
var time = DateTime.ParseExact(items[3], "dd/MM/yyyy HH:mm:ss", CultureInfo.InvariantCulture);
var callDataPoint = new IndicatorDataPoint(call, time, decimal.Parse(items[5], NumberStyles.Any, CultureInfo.InvariantCulture));
var putDataPoint = new IndicatorDataPoint(put, time, decimal.Parse(items[4], NumberStyles.Any, CultureInfo.InvariantCulture));
var underlyingDataPoint = new IndicatorDataPoint(call.Underlying, time, decimal.Parse(items[^4], NumberStyles.Any, CultureInfo.InvariantCulture));
callIndicator.Update(callDataPoint);
callIndicator.Update(underlyingDataPoint);
if (callIndicator.UseMirrorContract)
{
callIndicator.Update(putDataPoint);
}
var expected = double.Parse(items[callColumn], NumberStyles.Any, CultureInfo.InvariantCulture);
var acceptance = Math.Max(errorRate * Math.Abs(expected), errorMargin); // percentage error
Assert.AreEqual(expected, (double)callIndicator.Current.Value, acceptance);
putIndicator.Update(putDataPoint);
putIndicator.Update(underlyingDataPoint);
if (putIndicator.UseMirrorContract)
{
putIndicator.Update(callDataPoint);
}
expected = double.Parse(items[putColumn], NumberStyles.Any, CultureInfo.InvariantCulture);
acceptance = Math.Max(errorRate * Math.Abs(expected), errorMargin); // percentage error
Assert.AreEqual(expected, (double)putIndicator.Current.Value, acceptance);
}
protected override List<Symbol> GetSymbols()
{
return [Symbols.GOOG];
}
[Test]
public void ZeroGreeksIfExpired()
{
var indicator = CreateIndicator();
var date = new DateTime(2099, 1, 1); // date that the option must be expired already
var price = 500m;
var optionPrice = 10m;
indicator.Update(new IndicatorDataPoint(_symbol, date, optionPrice));
indicator.Update(new IndicatorDataPoint(_underlying, date, price));
Assert.AreEqual(0m, indicator.Current.Value);
}
[Test]
public override void ResetsProperly()
{
var indicator = CreateIndicator();
for (var i = 0; i < 5; i++)
{
var price = 500m;
var optionPrice = Math.Max(price - 450, 0) * 1.1m;
indicator.Update(new IndicatorDataPoint(_symbol, _reference.AddDays(1 + i), optionPrice));
indicator.Update(new IndicatorDataPoint(_underlying, _reference.AddDays(1 + i), price));
}
Assert.IsTrue(indicator.IsReady);
indicator.Reset();
TestHelper.AssertIndicatorIsInDefaultState(indicator);
}
[Test]
public override void TimeMovesForward()
{
var indicator = CreateIndicator();
for (var i = 10; i > 0; i--)
{
var price = 500m;
var optionPrice = Math.Max(price - 450, 0) * 1.1m;
indicator.Update(new IndicatorDataPoint(_symbol, _reference.AddDays(1 + i), optionPrice));
indicator.Update(new IndicatorDataPoint(_underlying, _reference.AddDays(1 + i), price));
}
Assert.AreEqual(2, indicator.Samples);
}
[Test]
public override void WarmsUpProperly()
{
var indicator = CreateIndicator();
var warmUpPeriod = (indicator as IIndicatorWarmUpPeriodProvider)?.WarmUpPeriod;
if (!warmUpPeriod.HasValue)
{
Assert.Ignore($"{indicator.Name} is not IIndicatorWarmUpPeriodProvider");
return;
}
// warmup period is 5 + 1
for (var i = 1; i <= warmUpPeriod.Value; i++)
{
var time = _reference.AddDays(i);
var price = 500m;
var optionPrice = Math.Max(price - 450, 0) * 1.1m;
indicator.Update(new IndicatorDataPoint(_symbol, time, optionPrice));
Assert.IsFalse(indicator.IsReady);
indicator.Update(new IndicatorDataPoint(_underlying, time, price));
Assert.IsTrue(indicator.IsReady);
}
Assert.AreEqual(2 * warmUpPeriod.Value, indicator.Samples);
}
[Test]
public override void WarmUpIndicatorProducesConsistentResults()
{
var algo = CreateAlgorithm();
algo.SetStartDate(2015, 12, 24);
algo.SetEndDate(2015, 12, 24);
var underlying = Symbols.GOOG;
var expiration = new DateTime(2015, 12, 24);
var strike = 650m;
var option = Symbol.CreateOption(underlying, Market.USA, OptionStyle.American, OptionRight.Put, strike, expiration);
SymbolList = [option];
var symbolsForWarmUp = new List<Symbol> { option, option.Underlying };
// Define the risk-free rate and dividend yield models
var risk = new ConstantRiskFreeRateInterestRateModel(12);
var dividend = new ConstantDividendYieldModel(12);
// Create the first indicator using the risk and dividend models
var firstIndicator = CreateIndicator(risk, dividend);
var period = (firstIndicator as IIndicatorWarmUpPeriodProvider)?.WarmUpPeriod;
if (period == null || period == 0)
{
Assert.Ignore($"{firstIndicator.Name}, Skipping this test because it's not applicable.");
}
// Warm up the first indicator
algo.WarmUpIndicator(symbolsForWarmUp, firstIndicator, Resolution.Daily);
// Warm up the second indicator manually
var secondIndicator = CreateIndicator(risk, dividend);
var history = algo.History(symbolsForWarmUp, period.Value, Resolution.Daily).ToList();
foreach (var slice in history)
{
foreach (var symbol in symbolsForWarmUp)
{
secondIndicator.Update(slice[symbol]);
}
}
SymbolList.Clear();
// Assert that the indicators are ready
Assert.IsTrue(firstIndicator.IsReady);
Assert.IsTrue(secondIndicator.IsReady);
if (!ValueCanBeZero)
{
Assert.AreNotEqual(firstIndicator.Current.Value, 0);
}
// Ensure that the first indicator has processed some data
Assert.AreNotEqual(firstIndicator.Samples, 0);
// Validate that both indicators have the same number of processed samples
Assert.AreEqual(firstIndicator.Samples, secondIndicator.Samples);
// Validate that both indicators produce the same final computed value
Assert.AreEqual(firstIndicator.Current.Value, secondIndicator.Current.Value);
}
[Test]
public override void WorksWithLowValues()
{
Symbol = _symbol;
base.WorksWithLowValues();
}
[Test]
public void UsesRiskFreeInterestRateModel()
{
const int count = 20;
var dates = Enumerable.Range(0, count).Select(i => new DateTime(2022, 11, 21, 10, 0, 0) + TimeSpan.FromDays(i)).ToList();
var interestRateValues = Enumerable.Range(0, count).Select(i => 0m + (10 - 0m) * (i / (count - 1m))).ToList();
var interestRateProviderMock = new Mock<IRiskFreeInterestRateModel>();
// Set up
for (int i = 0; i < count; i++)
{
interestRateProviderMock.Setup(x => x.GetInterestRate(dates[i])).Returns(interestRateValues[i]).Verifiable();
}
var indicator = CreateIndicator(interestRateProviderMock.Object);
for (int i = 0; i < count; i++)
{
indicator.Update(new IndicatorDataPoint(_symbol, dates[i], 80m + i));
indicator.Update(new IndicatorDataPoint(_underlying, dates[i], 500m + i));
Assert.AreEqual(interestRateValues[i], indicator.RiskFreeRate.Current.Value);
}
// Assert
Assert.IsTrue(indicator.IsReady);
interestRateProviderMock.Verify(x => x.GetInterestRate(It.IsAny<DateTime>()), Times.Exactly(dates.Count * RiskFreeRateUpdatesPerIteration));
for (int i = 0; i < count; i++)
{
interestRateProviderMock.Verify(x => x.GetInterestRate(dates[i]), Times.Exactly(RiskFreeRateUpdatesPerIteration));
}
}
[Test]
public void UsesPythonDefinedRiskFreeInterestRateModel()
{
using var _ = Py.GIL();
var module = PyModule.FromString(Guid.NewGuid().ToString(), $@"
from AlgorithmImports import *
class TestRiskFreeInterestRateModel:
CallCount = 0
def GetInterestRate(self, date: datetime) -> float:
TestRiskFreeInterestRateModel.CallCount += 1
return 0.5
def getOptionIndicatorBaseIndicator(symbol: Symbol) -> OptionIndicatorBase:
return {typeof(T).Name}(symbol, TestRiskFreeInterestRateModel())
");
var iv = module.GetAttr("getOptionIndicatorBaseIndicator").Invoke(_symbol.ToPython()).GetAndDispose<T>();
var modelClass = module.GetAttr("TestRiskFreeInterestRateModel");
var reference = new DateTime(2022, 11, 21, 10, 0, 0);
for (int i = 0; i < 20; i++)
{
iv.Update(new IndicatorDataPoint(_symbol, reference + TimeSpan.FromMinutes(i), 10m + i));
iv.Update(new IndicatorDataPoint(_underlying, reference + TimeSpan.FromMinutes(i), 1000m + i));
Assert.AreEqual((i + 1) * RiskFreeRateUpdatesPerIteration, modelClass.GetAttr("CallCount").GetAndDispose<int>());
}
}
[Test]
public void OptionIndicatorUsesAlgorithmsRiskFreeRateModelSetAfterIndicatorRegistration()
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
var historyProvider = new SubscriptionDataReaderHistoryProvider();
historyProvider.Initialize(new HistoryProviderInitializeParameters(null, null,
TestGlobals.DataProvider, TestGlobals.DataCacheProvider, TestGlobals.MapFileProvider, TestGlobals.FactorFileProvider,
null, true, new DataPermissionManager(), algorithm.ObjectStore, algorithm.Settings));
algorithm.SetHistoryProvider(historyProvider);
algorithm.SetDateTime(_reference);
algorithm.AddEquity(_underlying.Value);
algorithm.AddOptionContract(_symbol);
algorithm.Settings.AutomaticIndicatorWarmUp = true;
// Register indicator
var indicator = CreateIndicator(algorithm);
// Setup risk free rate model
var interestRateProviderMock = new Mock<IRiskFreeInterestRateModel>();
interestRateProviderMock.Setup(x => x.GetInterestRate(_reference)).Verifiable();
// Update indicator
indicator.Update(new IndicatorDataPoint(_symbol, _reference, 30m));
indicator.Update(new IndicatorDataPoint(_underlying, _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
indicator.Update(new IndicatorDataPoint(_symbol, _reference.AddDays(1), 30m));
indicator.Update(new IndicatorDataPoint(_underlying, _reference.AddDays(1), 300m));
// Our interest rate provider should have been called once by each update
interestRateProviderMock.Verify(x => x.GetInterestRate(_reference.AddDays(1)), Times.Exactly(RiskFreeRateUpdatesPerIteration));
}
[Test]
public void UsesDividendYieldModel()
{
const int count = 20;
var dates = Enumerable.Range(0, count).Select(i => new DateTime(2022, 11, 21, 10, 0, 0) + TimeSpan.FromDays(i)).ToList();
var dividends = Enumerable.Range(0, count).Select(i => 0m + (10 - 0m) * (i / (count - 1m))).ToList();
var dividendYieldProviderMock = new Mock<IDividendYieldModel>();
// Set up
var underlyingBasePrice = 500m;
for (int i = 0; i < count; i++)
{
dividendYieldProviderMock.Setup(x => x.GetDividendYield(dates[i], underlyingBasePrice + i)).Returns(dividends[i]).Verifiable();
}
var indicator = CreateIndicator(new ConstantRiskFreeRateInterestRateModel(0.05m), dividendYieldProviderMock.Object);
for (int i = 0; i < count; i++)
{
indicator.Update(new IndicatorDataPoint(_symbol, dates[i], 80m + i));
indicator.Update(new IndicatorDataPoint(_underlying, dates[i], underlyingBasePrice + i));
Assert.AreEqual(dividends[i], indicator.DividendYield.Current.Value);
}
// Assert
Assert.IsTrue(indicator.IsReady);
dividendYieldProviderMock.Verify(x => x.GetDividendYield(It.IsAny<DateTime>(), It.IsAny<decimal>()), Times.Exactly(dates.Count * DividendYieldUpdatesPerIteration));
for (int i = 0; i < count; i++)
{
dividendYieldProviderMock.Verify(x => x.GetDividendYield(dates[i], underlyingBasePrice + i), Times.Exactly(DividendYieldUpdatesPerIteration));
}
}
[Test]
public void UsesPythonDefinedDividendYieldModel()
{
using var _ = Py.GIL();
var module = PyModule.FromString(Guid.NewGuid().ToString(), $@"
from AlgorithmImports import *
class TestDividendYieldModel:
call_count = 0
def get_dividend_yield(self, date: datetime, price: float) -> float:
TestDividendYieldModel.call_count += 1
return 0.5
def get_option_indicator_base_indicator(symbol: Symbol) -> OptionIndicatorBase:
return {typeof(T).Name}(symbol, InterestRateProvider(), TestDividendYieldModel())
");
var indicator = module.GetAttr("get_option_indicator_base_indicator").Invoke(_symbol.ToPython()).GetAndDispose<T>();
var modelClass = module.GetAttr("TestDividendYieldModel");
var reference = new DateTime(2022, 11, 21, 10, 0, 0);
for (int i = 0; i < 20; i++)
{
indicator.Update(new IndicatorDataPoint(_symbol, reference + TimeSpan.FromMinutes(i), 10m + i));
indicator.Update(new IndicatorDataPoint(_underlying, reference + TimeSpan.FromMinutes(i), 1000m + i));
Assert.AreEqual((i + 1) * DividendYieldUpdatesPerIteration, modelClass.GetAttr("call_count").GetAndDispose<int>());
}
}
// Not used
protected override string TestFileName => null;
protected override string TestColumnName => null;
[Test]
public override void ComparesAgainstExternalData()
{
// Not used
}
[Test]
public override void ComparesAgainstExternalDataAfterReset()
{
// Not used
}
public override void AcceptsRenkoBarsAsInput()
{
// Not used
}
public override void AcceptsVolumeRenkoBarsAsInput()
{
// Not used
}
}
}