Skip to content

Commit 26ef023

Browse files
authored
Fix QuantBook Option/Future history api date range (#8671)
* Fix QuantBook Option/Future history api date range Properly calculate date range when passed range is not a whole date but a time of day * Extended unit tests
1 parent 68ec0b7 commit 26ef023

2 files changed

Lines changed: 160 additions & 11 deletions

File tree

Research/QuantBook.cs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ public OptionHistory OptionHistory(Symbol symbol, DateTime start, DateTime? end
420420
// only add underlying if not present
421421
AddIndex(symbol.Underlying.Value, resolutionToUseForUnderlying, fillForward: fillForward);
422422
}
423-
else if(symbol.Underlying.SecurityType == SecurityType.Future && symbol.Underlying.IsCanonical())
423+
else if (symbol.Underlying.SecurityType == SecurityType.Future && symbol.Underlying.IsCanonical())
424424
{
425425
AddFuture(symbol.Underlying.ID.Symbol, resolutionToUseForUnderlying, fillForward: fillForward,
426426
extendedMarketHours: extendedMarketHours);
@@ -435,13 +435,11 @@ public OptionHistory OptionHistory(Symbol symbol, DateTime start, DateTime? end
435435
var allSymbols = new HashSet<Symbol>();
436436
var optionFilterUniverse = new OptionFilterUniverse(option);
437437

438-
foreach (var date in QuantConnect.Time.EachTradeableDay(option, start, end.Value.AddDays(-1), extendedMarketHours))
438+
foreach (var (date, chainData, underlyingData) in GetChainHistory<OptionUniverse>(option, start, end.Value, extendedMarketHours))
439439
{
440-
var universeData = GetChainHistory<OptionUniverse>(symbol, date, out var underlyingData);
441-
442440
if (underlyingData is not null)
443441
{
444-
optionFilterUniverse.Refresh(universeData, underlyingData, underlyingData.EndTime);
442+
optionFilterUniverse.Refresh(chainData, underlyingData, underlyingData.EndTime);
445443
allSymbols.UnionWith(option.ContractFilter.Filter(optionFilterUniverse).Select(x => x.Symbol));
446444
}
447445
}
@@ -499,13 +497,9 @@ public FutureHistory FutureHistory(Symbol symbol, DateTime start, DateTime? end
499497
// canonical symbol, lets find the contracts
500498
var future = Securities[symbol] as Future;
501499

502-
for (var date = start; date < end; date = date.AddDays(1))
500+
foreach (var (date, chainData, underlyingData) in GetChainHistory<FutureUniverse>(future, start, end.Value, extendedMarketHours))
503501
{
504-
if (future.Exchange.DateIsOpen(date, extendedMarketHours))
505-
{
506-
var universeData = GetChainHistory<FutureUniverse>(future.Symbol, date, out _);
507-
allSymbols.UnionWith(future.ContractFilter.Filter(new FutureFilterUniverse(universeData, date)).Select(x => x.Symbol));
508-
}
502+
allSymbols.UnionWith(future.ContractFilter.Filter(new FutureFilterUniverse(chainData, date)).Select(x => x.Symbol));
509503
}
510504
}
511505
else
@@ -882,6 +876,20 @@ private IEnumerable<T> GetChainHistory<T>(Symbol canonicalSymbol, DateTime date,
882876
return Enumerable.Empty<T>();
883877
}
884878

879+
/// <summary>
880+
/// Helper method to get option/future chain historical data for a given date range
881+
/// </summary>
882+
private IEnumerable<(DateTime Date, IEnumerable<T> ChainData, BaseData UnderlyingData)> GetChainHistory<T>(
883+
Security security, DateTime start, DateTime end, bool extendedMarketHours)
884+
where T : BaseChainUniverseData
885+
{
886+
foreach (var date in QuantConnect.Time.EachTradeableDay(security, start.Date, end.Date, extendedMarketHours))
887+
{
888+
var universeData = GetChainHistory<T>(security.Symbol, date, out var underlyingData);
889+
yield return (date, universeData, underlyingData);
890+
}
891+
}
892+
885893
/// <summary>
886894
/// Helper method to perform selection on the given data and filter it
887895
/// </summary>

Tests/Research/QuantBookHistoryTests.cs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
using QuantConnect.Research;
2323
using QuantConnect.Logging;
2424
using QuantConnect.Data.Fundamental;
25+
using System.Data;
26+
using QuantConnect.Securities.Future;
27+
using QuantConnect.Data;
28+
using NodaTime;
29+
using QuantConnect.Interfaces;
30+
using QuantConnect.Data.UniverseSelection;
2531

2632
namespace QuantConnect.Tests.Research
2733
{
@@ -179,6 +185,65 @@ public void CanonicalOptionIntradayQuantBookHistory()
179185
}
180186
}
181187

188+
private static TestCaseData[] CanonicalOptionIntradayHistoryTestCases
189+
{
190+
get
191+
{
192+
var twx = Symbol.Create("TWX", SecurityType.Equity, Market.USA);
193+
var twxOption = Symbol.CreateCanonicalOption(twx);
194+
195+
return
196+
[
197+
new TestCaseData(twxOption, new DateTime(2014, 06, 05), (DateTime?)null),
198+
new TestCaseData(twxOption, new DateTime(2014, 06, 05), new DateTime(2014, 06, 05)),
199+
new TestCaseData(twxOption, new DateTime(2014, 06, 05), new DateTime(2014, 06, 06)),
200+
new TestCaseData(twxOption, new DateTime(2014, 06, 05, 0, 0, 0), new DateTime(2014, 06, 05, 15, 0, 0)),
201+
new TestCaseData(twxOption, new DateTime(2014, 06, 05, 10, 0, 0), new DateTime(2014, 06, 05, 15, 0, 0)),
202+
new TestCaseData(twxOption, new DateTime(2014, 06, 05, 10, 0, 0), new DateTime(2014, 06, 06)),
203+
new TestCaseData(twxOption, new DateTime(2014, 06, 05, 10, 0, 0), new DateTime(2014, 06, 06, 10, 0, 0)),
204+
new TestCaseData(twxOption, new DateTime(2014, 06, 05, 10, 0, 0), new DateTime(2014, 06, 06, 15, 0, 0))
205+
];
206+
}
207+
}
208+
209+
[TestCaseSource(nameof(CanonicalOptionIntradayHistoryTestCases))]
210+
public void CanonicalOptionIntradayQuantBookHistoryWithIntradayRange(Symbol canonicalOption, DateTime start, DateTime? end)
211+
{
212+
var quantBook = new QuantBook();
213+
var historyProvider = new TestHistoryProvider(quantBook.HistoryProvider);
214+
quantBook.SetHistoryProvider(historyProvider);
215+
quantBook.SetStartDate((end ?? start).Date.AddDays(1));
216+
217+
var option = quantBook.AddSecurity(canonicalOption);
218+
var history = quantBook.OptionHistory(canonicalOption, start, end, Resolution.Minute);
219+
220+
Assert.Greater(history.Count, 0);
221+
222+
var symbolsInHistory = history.SelectMany(slice => slice.AllData.Select(x => x.Symbol)).Distinct().ToList();
223+
Assert.Greater(symbolsInHistory.Count, 1);
224+
225+
var underlying = symbolsInHistory.Where(x => x == canonicalOption.Underlying).ToList();
226+
Assert.AreEqual(1, underlying.Count);
227+
228+
var contractsSymbols = symbolsInHistory.Where(x => x.SecurityType == canonicalOption.SecurityType).ToList();
229+
Assert.Greater(contractsSymbols.Count, 1);
230+
231+
var expectedDates = new HashSet<DateTime> { start.Date };
232+
if (end.HasValue && end.Value > end.Value.Date)
233+
{
234+
expectedDates.Add(end.Value.Date);
235+
}
236+
237+
var dataDates = history.SelectMany(slice => slice.AllData.Where(x => contractsSymbols.Contains(x.Symbol)).Select(x => x.EndTime.Date)).ToHashSet();
238+
CollectionAssert.AreEqual(expectedDates, dataDates);
239+
240+
// OptionUniverse must have been requested for all dates in the range
241+
foreach (var date in Time.EachTradeableDay(option, start.Date, (end ?? start).Date))
242+
{
243+
Assert.AreEqual(1, historyProvider.HistoryRequests.Count(request => request.DataType == typeof(OptionUniverse) && request.EndTimeLocal == date));
244+
}
245+
}
246+
182247
[Test]
183248
public void OptionContractQuantBookHistory()
184249
{
@@ -301,6 +366,57 @@ public void CanonicalFutureIntradayQuantBookHistory(int maxFilter, int numberOfF
301366
}
302367
}
303368

369+
private static TestCaseData[] CanonicalFutureIntradayHistoryTestCases
370+
{
371+
get
372+
{
373+
var es = Symbol.Create(Futures.Indices.SP500EMini, SecurityType.Future, Market.CME);
374+
return
375+
[
376+
new TestCaseData(es, new DateTime(2013, 10, 10), (DateTime?)null),
377+
new TestCaseData(es, new DateTime(2013, 10, 10), new DateTime(2013, 10, 10)),
378+
new TestCaseData(es, new DateTime(2013, 10, 10), new DateTime(2013, 10, 11)),
379+
new TestCaseData(es, new DateTime(2013, 10, 10, 0, 0, 0), new DateTime(2013, 10, 10, 15, 0, 0)),
380+
new TestCaseData(es, new DateTime(2013, 10, 10, 10, 0, 0), new DateTime(2013, 10, 10, 15, 0, 0)),
381+
new TestCaseData(es, new DateTime(2013, 10, 10, 10, 0, 0), new DateTime(2013, 10, 11)),
382+
new TestCaseData(es, new DateTime(2013, 10, 10, 10, 0, 0), new DateTime(2013, 10, 11, 10, 0, 0)),
383+
new TestCaseData(es, new DateTime(2013, 10, 10, 10, 0, 0), new DateTime(2013, 10, 11, 15, 0, 0))
384+
];
385+
}
386+
}
387+
388+
[TestCaseSource(nameof(CanonicalFutureIntradayHistoryTestCases))]
389+
public void CanonicalFutureIntradayQuantBookHistoryWithIntradayRange(Symbol canonicalFuture, DateTime start, DateTime? end)
390+
{
391+
var quantBook = new QuantBook();
392+
var historyProvider = new TestHistoryProvider(quantBook.HistoryProvider);
393+
quantBook.SetHistoryProvider(historyProvider);
394+
quantBook.SetStartDate((end ?? start).Date.AddDays(1));
395+
var future = quantBook.AddSecurity(canonicalFuture) as Future;
396+
future.SetFilter(universe => universe);
397+
398+
var history = quantBook.FutureHistory(canonicalFuture, start, end, Resolution.Minute);
399+
Assert.Greater(history.Count, 0);
400+
401+
var symbolsInHistory = history.SelectMany(slice => slice.AllData.Select(x => x.Symbol)).Distinct().ToList();
402+
Assert.Greater(symbolsInHistory.Count, 1);
403+
404+
var expectedDates = new HashSet<DateTime> { start.Date };
405+
if (end.HasValue && end.Value > end.Value.Date)
406+
{
407+
expectedDates.Add(end.Value.Date);
408+
}
409+
410+
var dataDates = history.SelectMany(slice => slice.AllData.Select(x => x.EndTime.Date)).ToHashSet();
411+
CollectionAssert.AreEqual(expectedDates, dataDates);
412+
413+
// FutureUniverse must have been requested for all dates in the range
414+
foreach (var date in Time.EachTradeableDay(future, start.Date, (end ?? start).Date))
415+
{
416+
Assert.AreEqual(1, historyProvider.HistoryRequests.Count(request => request.DataType == typeof(FutureUniverse) && request.EndTimeLocal == date));
417+
}
418+
}
419+
304420
[Test]
305421
public void FutureContractQuantBookHistory()
306422
{
@@ -685,5 +801,30 @@ def getHistory():
685801
Assert.IsFalse(pyHistory.HasAttr("data"));
686802
}
687803
}
804+
805+
private class TestHistoryProvider : HistoryProviderBase
806+
{
807+
private IHistoryProvider _provider;
808+
809+
public List<HistoryRequest> HistoryRequests { get; } = new();
810+
811+
public override int DataPointCount => _provider.DataPointCount;
812+
813+
public TestHistoryProvider(IHistoryProvider provider)
814+
{
815+
_provider = provider;
816+
}
817+
818+
public override void Initialize(HistoryProviderInitializeParameters parameters)
819+
{
820+
}
821+
822+
public override IEnumerable<Slice> GetHistory(IEnumerable<HistoryRequest> requests, DateTimeZone sliceTimeZone)
823+
{
824+
requests = requests.ToList();
825+
HistoryRequests.AddRange(requests);
826+
return _provider.GetHistory(requests, sliceTimeZone);
827+
}
828+
}
688829
}
689830
}

0 commit comments

Comments
 (0)