326 lines
14 KiB
C#
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 };
|
|
}
|
|
}
|
|
}
|