diff --git a/Common/Data/Consolidators/OpenInterestConsolidator.cs b/Common/Data/Consolidators/OpenInterestConsolidator.cs index d69fd4b25d9d..c561c265c30c 100644 --- a/Common/Data/Consolidators/OpenInterestConsolidator.cs +++ b/Common/Data/Consolidators/OpenInterestConsolidator.cs @@ -24,6 +24,10 @@ namespace QuantConnect.Data.Consolidators /// public class OpenInterestConsolidator : PeriodCountConsolidatorBase { + private bool _hourOrDailyConsolidation; + // Keep track of the last input to detect hour or date change + private Tick _lastInput; + /// /// Create a new OpenInterestConsolidator for the desired resolution /// @@ -41,6 +45,7 @@ public static OpenInterestConsolidator FromResolution(Resolution resolution) public OpenInterestConsolidator(TimeSpan period) : base(period) { + _hourOrDailyConsolidation = period >= Time.OneHour; } /// @@ -104,7 +109,7 @@ protected override void AggregateBar(ref OpenInterest workingBar, Tick data) workingBar = new OpenInterest { Symbol = data.Symbol, - Time = GetRoundedBarTime(data), + Time = _hourOrDailyConsolidation ? data.EndTime : GetRoundedBarTime(data), Value = data.Value }; @@ -113,7 +118,37 @@ protected override void AggregateBar(ref OpenInterest workingBar, Tick data) { //Update the working bar workingBar.Value = data.Value; + + // If we are consolidating hourly or daily, we need to update the time of the working bar + // for the end time to match the last data point time + if (_hourOrDailyConsolidation) + { + workingBar.Time = data.EndTime; + } } } + + /// + /// Updates this consolidator with the specified data. This method is + /// responsible for raising the DataConsolidated event. + /// It will check for date or hour change and force consolidation if needed. + /// + /// The new data for the consolidator + public override void Update(Tick data) + { + if (_lastInput != null && + _hourOrDailyConsolidation && + // Detect hour or date change + ((Period == Time.OneHour && data.EndTime.Hour != _lastInput.EndTime.Hour) || + (Period == Time.OneDay && data.EndTime.Date != _lastInput.EndTime.Date))) + { + // Date or hour change, force consolidation, no need to wait for the whole period to pass. + // Force consolidation by scanning at a time after the end of the period + Scan(_lastInput.EndTime.Add(Period.Value + Time.OneSecond)); + } + + base.Update(data); + _lastInput = data; + } } } diff --git a/Tests/Common/Data/OpenInterestConsolidatorTests.cs b/Tests/Common/Data/OpenInterestConsolidatorTests.cs new file mode 100644 index 000000000000..e1a32dd005d4 --- /dev/null +++ b/Tests/Common/Data/OpenInterestConsolidatorTests.cs @@ -0,0 +1,170 @@ +/* + * 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 NUnit.Framework; +using QuantConnect.Data; +using QuantConnect.Data.Consolidators; +using QuantConnect.Data.Market; +using QuantConnect.Logging; + +namespace QuantConnect.Tests.Common.Data +{ + [TestFixture] + public class OpenInterestConsolidatorTests : BaseConsolidatorTests + { + [TestCaseSource(nameof(HourAndDailyTestValues))] + public void HourAndDailyConsolidationKeepsTimeOfDay(TimeSpan period, List<(OpenInterest, bool)> data) + { + using var consolidator = new OpenInterestConsolidator(period); + + var consolidatedOpenInterest = (OpenInterest)null; + consolidator.DataConsolidated += (sender, consolidated) => + { + Log.Debug($"{consolidated.EndTime} - {consolidated}"); + consolidatedOpenInterest = consolidated; + }; + + var prevData = (OpenInterest)null; + foreach (var (openInterest, shouldConsolidate) in data) + { + consolidator.Update(openInterest); + + if (shouldConsolidate) + { + Assert.IsNotNull(consolidatedOpenInterest); + Assert.AreEqual(prevData.Symbol, consolidatedOpenInterest.Symbol); + Assert.AreEqual(prevData.Value, consolidatedOpenInterest.Value); + Assert.AreEqual(prevData.EndTime, consolidatedOpenInterest.EndTime); + consolidatedOpenInterest = null; + } + else + { + Assert.IsNull(consolidatedOpenInterest); + } + + prevData = openInterest; + } + } + + protected override IDataConsolidator CreateConsolidator() + { + return new OpenInterestConsolidator(TimeSpan.FromDays(1)); + } + + protected override IEnumerable GetTestValues() + { + var time = new DateTime(2015, 04, 13, 8, 31, 0); + return new List() + { + new OpenInterest(){ Time = time, Symbol = Symbols.SPY, Value = 10 }, + new OpenInterest(){ Time = time.AddMinutes(1), Symbol = Symbols.SPY, Value = 12 }, + new OpenInterest(){ Time = time.AddMinutes(2), Symbol = Symbols.SPY, Value = 10 }, + new OpenInterest(){ Time = time.AddMinutes(3), Symbol = Symbols.SPY, Value = 5 }, + new OpenInterest(){ Time = time.AddMinutes(4), Symbol = Symbols.SPY, Value = 15 }, + new OpenInterest(){ Time = time.AddMinutes(5), Symbol = Symbols.SPY, Value = 20 }, + new OpenInterest(){ Time = time.AddMinutes(6), Symbol = Symbols.SPY, Value = 18 }, + new OpenInterest(){ Time = time.AddMinutes(7), Symbol = Symbols.SPY, Value = 12 }, + new OpenInterest(){ Time = time.AddMinutes(8), Symbol = Symbols.SPY, Value = 25 }, + new OpenInterest(){ Time = time.AddMinutes(9), Symbol = Symbols.SPY, Value = 30 }, + new OpenInterest(){ Time = time.AddMinutes(10), Symbol = Symbols.SPY, Value = 26 }, + }; + } + + private static IEnumerable HourAndDailyTestValues() + { + var symbol = Symbols.SPY_C_192_Feb19_2016; + var time = new DateTime(2015, 04, 13, 6, 30, 0); + var period = Time.OneDay; + + yield return new TestCaseData( + period, + new List<(OpenInterest, bool)>() + { + (new OpenInterest(time, symbol, 10), false), + (new OpenInterest(time.AddDays(1), symbol, 11), true), + (new OpenInterest(time.AddDays(2), symbol, 12), true), + (new OpenInterest(time.AddDays(3), symbol, 13), true), + (new OpenInterest(time.AddDays(4), symbol, 14), true), + (new OpenInterest(time.AddDays(5), symbol, 15), true), + }); + + yield return new TestCaseData( + period, + new List<(OpenInterest, bool)>() + { + (new OpenInterest(time, symbol, 10), false), + (new OpenInterest(time.AddDays(1), symbol, 11), true), + // Same date, should not consolidate + (new OpenInterest(time.AddDays(1).AddMinutes(1), symbol, 12), false), + // Same date, should not consolidate + (new OpenInterest(time.AddDays(1).AddMinutes(2), symbol, 13), false), + // Same date, should not consolidate + (new OpenInterest(time.AddDays(1).AddMinutes(3), symbol, 14), false), + // Not the full period passed but different date, should consolidate + (new OpenInterest(time.AddDays(2).AddHours(-1), symbol, 15), true), + (new OpenInterest(time.AddDays(3).AddHours(-2), symbol, 16), true), + (new OpenInterest(time.AddDays(4).AddHours(-3), symbol, 17), true), + (new OpenInterest(time.AddDays(5).AddHours(-4), symbol, 18), true), + }); + + period = Time.OneHour; + + yield return new TestCaseData( + period, + new List<(OpenInterest, bool)>() + { + (new OpenInterest(time, symbol, 10), false), + (new OpenInterest(time.AddHours(1), symbol, 11), true), + (new OpenInterest(time.AddHours(2), symbol, 12), true), + (new OpenInterest(time.AddHours(3), symbol, 13), true), + (new OpenInterest(time.AddHours(4), symbol, 14), true), + (new OpenInterest(time.AddHours(5), symbol, 15), true), + }); + + yield return new TestCaseData( + period, + new List<(OpenInterest, bool)>() + { + (new OpenInterest(time.AddHours(0.5).AddMinutes(10), symbol, 10), false), + (new OpenInterest(time.AddHours(2.5).AddMinutes(20), symbol, 11), true), + (new OpenInterest(time.AddHours(4.5).AddMinutes(30), symbol, 12), true), + (new OpenInterest(time.AddHours(6.5).AddMinutes(40), symbol, 13), true), + (new OpenInterest(time.AddHours(8.5), symbol, 14), true), + (new OpenInterest(time.AddHours(10.5).AddMinutes(50), symbol, 15), true), + }); + + yield return new TestCaseData( + period, + new List<(OpenInterest, bool)>() + { + (new OpenInterest(time, symbol, 10), false), + (new OpenInterest(time.AddHours(1), symbol, 11), true), + // Same date, should not consolidate + (new OpenInterest(time.AddHours(1).AddMinutes(5), symbol, 12), false), + // Same date, should not consolidate + (new OpenInterest(time.AddHours(1).AddMinutes(10), symbol, 13), false), + // Same date, should not consolidate + (new OpenInterest(time.AddHours(1).AddMinutes(15), symbol, 14), false), + // Not the full period passed but different date, should consolidate + (new OpenInterest(time.AddHours(2).AddMinutes(-5), symbol, 15), true), + (new OpenInterest(time.AddHours(3).AddMinutes(-10), symbol, 16), true), + (new OpenInterest(time.AddHours(4).AddMinutes(-15), symbol, 17), true), + (new OpenInterest(time.AddHours(5).AddMinutes(-20), symbol, 18), true), + }); + } + } +}