Files
quantconnect--lean/Tests/Common/Securities/Options/OptionChainProviderTests.cs
T
2026-07-13 13:02:50 +08:00

326 lines
14 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.Diagnostics;
using System.Linq;
using System.Threading;
using NUnit.Framework;
using QuantConnect.Data;
using QuantConnect.Interfaces;
using QuantConnect.Lean.Engine.DataFeeds;
using QuantConnect.Securities.Future;
using QuantConnect.Util;
namespace QuantConnect.Tests.Common.Securities.Options
{
[TestFixture, Parallelizable(ParallelScope.Fixtures)]
public class OptionChainProviderTests
{
private BacktestingOptionChainProvider _backtestingOptionChainProvider;
private LiveOptionChainProvider _liveOptionChainProvider;
[OneTimeSetUp]
public void OneTimeSetUp()
{
_backtestingOptionChainProvider = new BacktestingOptionChainProvider();
_backtestingOptionChainProvider.Initialize(new(TestGlobals.MapFileProvider, TestGlobals.HistoryProvider));
_liveOptionChainProvider = new LiveOptionChainProvider();
_liveOptionChainProvider.Initialize(new(TestGlobals.MapFileProvider, TestGlobals.HistoryProvider));
}
[Test]
public void UsesMultipleResolutionsFutureOption()
{
// we don't have minute data for this date
var date = new DateTime(2020, 01, 7);
var future = Symbol.CreateFuture(QuantConnect.Securities.Futures.Indices.SP500EMini, Market.CME, new DateTime(2020, 6, 19));
var optionChain = _backtestingOptionChainProvider.GetOptionContractList(future, date).OrderBy(s => s.ID.StrikePrice).ToList();
Assert.IsTrue(optionChain.All(x => x.SecurityType == SecurityType.FutureOption));
Assert.IsTrue(optionChain.All(x => x.ID.Symbol == "ES"));
Assert.IsTrue(optionChain.All(x => x.Underlying == future));
Assert.IsTrue(optionChain.All(x => x.ID.Date.Date >= date));
Assert.AreEqual(107, optionChain.Count);
Assert.AreEqual(2900m, optionChain.First().ID.StrikePrice);
Assert.AreEqual(3500, optionChain.Last().ID.StrikePrice);
}
[Test]
public void BacktestingOptionChainProviderUsesPreviousTradableDateChain()
{
// the 7th is a saturday should fetch fridays data instead
var date = new DateTime(2014, 6, 7);
Assert.AreEqual(DayOfWeek.Saturday, date.DayOfWeek);
var twxOptionChain = _backtestingOptionChainProvider.GetOptionContractList(Symbol.Create("TWX", SecurityType.Equity, Market.USA), date)
.ToList();
Assert.AreEqual(184, twxOptionChain.Count);
Assert.AreEqual(23m, twxOptionChain.OrderBy(s => s.ID.StrikePrice).First().ID.StrikePrice);
Assert.AreEqual(105m, twxOptionChain.OrderBy(s => s.ID.StrikePrice).Last().ID.StrikePrice);
}
[Test]
public void BacktestingOptionChainProviderLoadsEquityOptionChain()
{
var twxOptionChain = _backtestingOptionChainProvider.GetOptionContractList(Symbol.Create("TWX", SecurityType.Equity, Market.USA), new DateTime(2014, 6, 5))
.ToList();
Assert.AreEqual(184, twxOptionChain.Count);
Assert.AreEqual(23m, twxOptionChain.OrderBy(s => s.ID.StrikePrice).First().ID.StrikePrice);
Assert.AreEqual(105m, twxOptionChain.OrderBy(s => s.ID.StrikePrice).Last().ID.StrikePrice);
}
[Test]
public void BacktestingOptionChainProviderLoadsFutureOptionChain()
{
var esOptionChain = _backtestingOptionChainProvider.GetOptionContractList(
Symbol.CreateFuture(
QuantConnect.Securities.Futures.Indices.SP500EMini,
Market.CME,
new DateTime(2020, 6, 19)),
new DateTime(2020, 1, 5))
.ToList();
Assert.AreEqual(107, esOptionChain.Count);
Assert.AreEqual(2900m, esOptionChain.OrderBy(s => s.ID.StrikePrice).First().ID.StrikePrice);
Assert.AreEqual(3500m, esOptionChain.OrderBy(s => s.ID.StrikePrice).Last().ID.StrikePrice);
}
[Test]
public void BacktestingOptionChainProviderIndexOption()
{
var spxOption = Symbol.CreateCanonicalOption(Symbols.SPX);
foreach (var option in new [] { Symbols.SPX, spxOption })
{
var optionChain = _backtestingOptionChainProvider.GetOptionContractList(option, new DateTime(2021, 01, 04)).ToList();
Assert.AreEqual(6, optionChain.Count);
Assert.AreEqual(3200, optionChain.OrderBy(s => s.ID.StrikePrice).First().ID.StrikePrice);
Assert.AreEqual(4250, optionChain.OrderBy(s => s.ID.StrikePrice).Last().ID.StrikePrice);
foreach (var optionSymbol in optionChain)
{
Assert.AreEqual("SPX", optionSymbol.ID.Symbol);
Assert.AreEqual("SPX", optionSymbol.Underlying.ID.Symbol);
}
}
}
[Test]
public void BacktestingOptionChainProviderWeeklyIndexOption()
{
var spxWeeklyOption = Symbol.CreateCanonicalOption(Symbols.SPX, "SPXW", null, null);
foreach (var option in new[] { spxWeeklyOption })
{
var optionChain = _backtestingOptionChainProvider.GetOptionContractList(option, new DateTime(2021, 01, 04)).ToList();
Assert.AreEqual(12, optionChain.Count);
Assert.AreEqual(3700, optionChain.OrderBy(s => s.ID.StrikePrice).First().ID.StrikePrice);
Assert.AreEqual(3800, optionChain.OrderBy(s => s.ID.StrikePrice).Last().ID.StrikePrice);
foreach (var optionSymbol in optionChain)
{
Assert.AreEqual("SPXW", optionSymbol.ID.Symbol);
Assert.AreEqual("SPX", optionSymbol.Underlying.ID.Symbol);
}
}
}
[Test]
public void BacktestingOptionChainProviderResolvesSymbolMapping()
{
var ticker = "GOOCV"; // Old ticker, should resolve and fetch GOOG
var underlyingSymbol = QuantConnect.Symbol.Create(ticker, SecurityType.Equity, Market.USA);
var alias = "?" + underlyingSymbol.Value;
var optionSymbol = Symbol.CreateOption(
underlyingSymbol,
underlyingSymbol.ID.Market,
Symbol.GetOptionTypeFromUnderlying(underlyingSymbol).DefaultOptionStyle(),
default(OptionRight),
0,
SecurityIdentifier.DefaultDate,
alias);
var googOptionChain = _backtestingOptionChainProvider.GetOptionContractList(optionSymbol.Underlying, new DateTime(2015, 12, 23))
.ToList();
Assert.AreEqual(118, googOptionChain.Count);
Assert.AreEqual(600m, googOptionChain.OrderBy(s => s.ID.StrikePrice).First().ID.StrikePrice);
Assert.AreEqual(800m, googOptionChain.OrderBy(s => s.ID.StrikePrice).Last().ID.StrikePrice);
}
[Test]
public void CachingProviderCachesSymbolsByDate()
{
var provider = new CachingOptionChainProvider(new DelayedOptionChainProvider(1000));
var stopwatch = Stopwatch.StartNew();
var symbols = provider.GetOptionContractList(Symbol.Empty, new DateTime(2017, 7, 28));
stopwatch.Stop();
Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, 1000);
Assert.AreEqual(2, symbols.Count());
stopwatch.Restart();
symbols = provider.GetOptionContractList(Symbol.Empty, new DateTime(2017, 7, 28));
stopwatch.Stop();
Assert.LessOrEqual(stopwatch.ElapsedMilliseconds, 10);
Assert.AreEqual(2, symbols.Count());
stopwatch.Restart();
symbols = provider.GetOptionContractList(Symbol.Empty, new DateTime(2017, 7, 29));
stopwatch.Stop();
Assert.GreaterOrEqual(stopwatch.ElapsedMilliseconds, 1000);
Assert.AreEqual(2, symbols.Count());
}
[Test]
public void LiveOptionChainProviderReturnsData()
{
var spxOption = Symbol.CreateCanonicalOption(Symbols.SPX);
var spxwOption = Symbol.CreateCanonicalOption(Symbols.SPX, "SPXW", null, null);
foreach (var symbol in new[] { Symbols.SPY, Symbols.AAPL, Symbols.MSFT, Symbols.SPX, spxOption, spxwOption })
{
var result = _liveOptionChainProvider.GetOptionContractList(symbol, DateTime.Today).ToList();
var countCall = result.Count(x => x.ID.OptionRight == OptionRight.Call);
var countPut = result.Count(x => x.ID.OptionRight == OptionRight.Put);
Assert.Greater(countCall, 0);
Assert.Greater(countPut, 0);
var expectedOptionTicker = symbol.ID.Symbol;
var expectedUnderlyingTicker = symbol.ID.Symbol;
if (symbol.ID.Symbol == "SPXW")
{
expectedUnderlyingTicker = "SPX";
}
foreach (var optionSymbol in result)
{
Assert.AreEqual(expectedOptionTicker, optionSymbol.ID.Symbol);
Assert.AreEqual(expectedUnderlyingTicker, optionSymbol.Underlying.ID.Symbol);
}
}
}
[Test]
public void LiveOptionChainProviderReturnsNoDataForInvalidSymbol()
{
var symbol = Symbol.Create("ABCDEF123", SecurityType.Equity, Market.USA);
var result = _liveOptionChainProvider.GetOptionContractList(symbol, DateTime.Today);
Assert.IsFalse(result.Any());
}
[Test]
[Category("TravisExclude")] // For now this test is excluded from the Travis build because of frequent forbidden 403 HTTP response from CME API
public void LiveOptionChainProviderReturnsFutureOptionData()
{
var now = DateTime.Now;
var december = new DateTime(now.Year, 12, 1);
var canonicalFuture = Symbol.Create("ES", SecurityType.Future, Market.CME);
var expiry = FuturesExpiryFunctions.FuturesExpiryFunction(canonicalFuture)(december);
// When the current year's december contract expires, the test starts failing.
// This will happen around the last 10 days of December, but will start working
// once we've crossed into the new year.
// Let's try the next listed contract, which is in March of the next year if this is the case.
if (now >= expiry)
{
expiry = now.AddMonths(-now.Month).AddYears(1).AddMonths(3);
}
var underlyingFuture = Symbol.CreateFuture("ES", Market.CME, expiry);
var result = _liveOptionChainProvider.GetOptionContractList(underlyingFuture, now).ToList();
Assert.AreNotEqual(0, result.Count);
foreach (var symbol in result)
{
Assert.IsTrue(symbol.HasUnderlying);
Assert.AreEqual(Market.CME, symbol.ID.Market);
Assert.AreEqual(OptionStyle.American, symbol.ID.OptionStyle);
Assert.GreaterOrEqual(symbol.ID.StrikePrice, 100m);
Assert.Less(symbol.ID.StrikePrice, 30000m);
}
}
[Test]
public void LiveOptionChainProviderReturnsNoDataForOldFuture()
{
var now = DateTime.Now;
var december = now.AddMonths(-now.Month).AddYears(-1);
var underlyingFuture = Symbol.CreateFuture("ES", Market.CME, december);
var result = _liveOptionChainProvider.GetOptionContractList(underlyingFuture, december);
Assert.AreEqual(0, result.Count());
}
[TestCase(OptionRight.Call, 1650, 2020, 3, 26)]
[TestCase(OptionRight.Put, 1540, 2020, 3, 26)]
[TestCase(OptionRight.Call, 1600, 2020, 2, 25)]
[TestCase(OptionRight.Put, 1545, 2020, 2, 25)]
public void BacktestingOptionChainProviderReturnsMultipleContractsForZipFileContainingMultipleContracts(
OptionRight right,
int strike,
int year,
int month,
int day)
{
var underlying = Symbol.CreateFuture("GC", Market.COMEX, new DateTime(2020, 4, 28));
var expiry = new DateTime(year, month, day);
var expectedOption = Symbol.CreateOption(
underlying,
Market.COMEX,
OptionStyle.American,
right,
strike,
expiry);
var contracts = _backtestingOptionChainProvider.GetOptionContractList(underlying, new DateTime(2020, 1, 5))
.ToHashSet();
Assert.IsTrue(
contracts.Contains(expectedOption),
$"Failed to find contract {expectedOption} in: [{string.Join(", ", contracts.Select(s => s.ToString()))}");
}
}
internal class DelayedOptionChainProvider : IOptionChainProvider
{
private readonly int _delayMilliseconds;
public DelayedOptionChainProvider(int delayMilliseconds)
{
_delayMilliseconds = delayMilliseconds;
}
public IEnumerable<Symbol> GetOptionContractList(Symbol symbol, DateTime date)
{
Thread.Sleep(_delayMilliseconds);
return new[] { Symbols.SPY_C_192_Feb19_2016, Symbols.SPY_P_192_Feb19_2016 };
}
}
}