Files
quantconnect--lean/Tests/Common/Data/PeriodCountConsolidatorTests.cs
2026-07-13 13:02:50 +08:00

410 lines
18 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 NUnit.Framework;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Market;
namespace QuantConnect.Tests.Common.Data
{
[TestFixture, Parallelizable(ParallelScope.All)]
public class PeriodCountConsolidatorTests
{
private static readonly object[] PeriodCases =
{
new [] { TimeSpan.FromDays(100), TimeSpan.FromDays(10) },
new [] { TimeSpan.FromDays(30), TimeSpan.FromDays(1) }, //GH Issue #4915
new [] { TimeSpan.FromDays(10), TimeSpan.FromDays(1) },
new [] { TimeSpan.FromDays(1), TimeSpan.FromHours(1) },
new [] { TimeSpan.FromHours(10), TimeSpan.FromHours(1) },
new [] { TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(1) },
new [] { TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(10) },
new [] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(0.1) }
};
[TestCaseSource(nameof(PeriodCases))]
public void ExpectedConsolidatedTradeBarsInPeriodMode(TimeSpan barSpan, TimeSpan updateSpan)
{
TradeBar consolidated = null;
using var consolidator = new BaseDataConsolidator(barSpan);
consolidator.DataConsolidated += (sender, bar) =>
{
Assert.AreEqual(barSpan, bar.Period); // The period matches our span
consolidated = bar;
};
var reference = new DateTime(2015, 04, 13);
var dataTime = reference;
var nextBarTime = reference + barSpan;
var lastBarTime = reference;
// First data point
consolidator.Update(new Tick { Time = dataTime });
Assert.IsNull(consolidated);
for (var i = 0; i < 10; i++)
{
// Add data on the given interval until we expect a new bar
while (dataTime < nextBarTime)
{
dataTime = dataTime.Add(updateSpan);
consolidator.Update(new Tick { Time = dataTime });
}
// Our asserts
Assert.IsNotNull(consolidated); // We have a bar
Assert.AreEqual(dataTime, consolidated.EndTime); // New bar time should be dataTime
Assert.AreEqual(barSpan, consolidated.EndTime - lastBarTime); // The difference between the bars is the span
nextBarTime = dataTime + barSpan;
lastBarTime = consolidated.EndTime;
}
}
[TestCaseSource(nameof(PeriodCases))]
public void ExpectedConsolidatedQuoteBarsInPeriodMode(TimeSpan barSpan, TimeSpan updateSpan)
{
QuoteBar consolidated = null;
using var consolidator = new QuoteBarConsolidator(barSpan);
consolidator.DataConsolidated += (sender, bar) =>
{
Assert.AreEqual(barSpan, bar.Period); // The period matches our span
consolidated = bar;
};
var reference = new DateTime(2015, 04, 13);
var dataTime = reference;
var nextBarTime = reference + barSpan;
var lastBarTime = reference;
// First data point
consolidator.Update(new QuoteBar { Time = dataTime, Period = updateSpan });
Assert.IsNull(consolidated);
for (var i = 0; i < 10; i++)
{
// Add data on the given interval until we expect a new bar
while (dataTime < nextBarTime)
{
dataTime = dataTime.Add(updateSpan);
consolidator.Update(new QuoteBar { Time = dataTime, Period = updateSpan });
}
// Our asserts
Assert.IsNotNull(consolidated); // We have a bar
Assert.AreEqual(dataTime, consolidated.EndTime); // New bar time should be dataTime
Assert.AreEqual(barSpan, consolidated.EndTime - lastBarTime); // The difference between the bars is the span
nextBarTime = dataTime + barSpan;
lastBarTime = consolidated.EndTime;
}
}
[Test]
public void ConsolidatorEmitsOffsetBarsCorrectly()
{
// This test is to cover an issue seen with the live data stack
// The consolidator would fail to emit every other bar because of a
// ms delay in data from a live stream
var period = TimeSpan.FromHours(2);
using var consolidator = new TradeBarConsolidator(period);
var consolidatedBarsCount = 0;
consolidator.DataConsolidated += (sender, bar) =>
{
consolidatedBarsCount++;
};
var random = new Random();
var time = new DateTime(2015, 04, 13);
// The bars time is accurate, covering the hour perfectly
// But the emit time is slightly offset (the timeslice that contains the bar)
// So add a random ms offset to the scan time
consolidator.Update(new TradeBar { Time = time, Period = Time.OneHour });
time = time.Add(period);
consolidator.Scan(time.AddMilliseconds(random.Next(800)));
consolidator.Update(new TradeBar { Time = time, Period = Time.OneHour });
time = time.Add(period);
consolidator.Scan(time.AddMilliseconds(random.Next(800)));
consolidator.Update(new TradeBar { Time = time, Period = Time.OneHour });
time = time.Add(period);
consolidator.Scan(time.AddMilliseconds(random.Next(800)));
consolidator.Update(new TradeBar { Time = time, Period = Time.OneHour });
time = time.Add(period);
consolidator.Scan(time.AddMilliseconds(random.Next(800)));
// We should expect to see 4 bars emitted from the consolidator
Assert.AreEqual(4, consolidatedBarsCount);
}
[Test]
public void ConsolidatorEmitsOldBarsUsingUpdate()
{
// This test is to ensure that no bars get swallowed by the consolidator
// even if it doesn't get the data on regular intervals.
// We will use the PushThrough method which calls update
var period = TimeSpan.FromHours(1);
using var consolidator = new TradeBarConsolidator(period);
TradeBar latestConsolidated = null;
var consolidatedBarsCount = 0;
consolidator.DataConsolidated += (sender, bar) =>
{
latestConsolidated = bar;
consolidatedBarsCount++;
};
// Set our starting time 04/13/2015 at 12:00AM
var time = new DateTime(2015, 04, 13);
// Update this consolidator with minute tradebars but one less than 60, which would trigger emit
PushBarsThrough(59, Time.OneMinute, consolidator, ref time);
// No bars should be emitted, lets assert the current time and count
Assert.IsTrue(time == new DateTime(2015, 04, 13, 0, 59, 0));
Assert.AreEqual(0, consolidatedBarsCount);
// Advance time way past (3 hours) the bar end time of 1AM
time += TimeSpan.FromHours(3); // Time = 3:59AM now
// Push one bar through at 3:59AM and check that we still get the 12AM - 1AM Bar emitted
PushBarsThrough(1, Time.OneMinute, consolidator, ref time);
Assert.AreEqual(1, consolidatedBarsCount);
Assert.IsTrue(latestConsolidated != null && latestConsolidated.Time == new DateTime(2015, 04, 13));
// Check the new working bar is 3AM to 4AM, This is because we pushed a bar in at 3:59AM
Assert.IsTrue(consolidator.WorkingBar.Time == new DateTime(2015, 04, 13, 3, 0, 0));
}
[Test]
public void ConsolidatorEmitsOldBarsUsingScan()
{
// This test is to ensure that no bars get swallowed by the consolidator
// even if it doesn't get the data on regular intervals.
// We will use Consolidators Scan method to emit bars
var period = TimeSpan.FromHours(1);
using var consolidator = new TradeBarConsolidator(period);
TradeBar latestConsolidated = null;
var consolidatedBarsCount = 0;
consolidator.DataConsolidated += (sender, bar) =>
{
latestConsolidated = bar;
consolidatedBarsCount++;
};
var time = new DateTime(2015, 04, 13);
// Push through one bar at 12:00AM to create the consolidators working bar
PushBarsThrough(1, Time.OneMinute, consolidator, ref time);
// There should be no emit, lets assert the current time and count
Assert.IsTrue(time == new DateTime(2015, 04, 13, 0, 1, 0));
Assert.AreEqual(0, consolidatedBarsCount);
// Now advance time way past (3 Hours) the bar end time of 1AM
time += TimeSpan.FromHours(3); // Time = 3:59AM now
// Call scan with current time, it should emit the 12AM - 1AM Bar without any update
consolidator.Scan(time);
Assert.AreEqual(1, consolidatedBarsCount);
Assert.IsTrue(latestConsolidated != null && latestConsolidated.Time == new DateTime(2015, 04, 13, 0, 0, 0));
// WorkingBar should be null, ready for whatever data comes through next
Assert.IsTrue(consolidator.WorkingBar == null);
}
[Test]
public void ConsolidatorEmitsRegularly()
{
// This test just pushes through 1000 bars
// and ensures that the emit time and count are correct
var period = TimeSpan.FromHours(2);
using var consolidator = new TradeBarConsolidator(period);
var consolidatedBarsCount = 0;
var time = new DateTime(2015, 04, 13);
consolidator.DataConsolidated += (sender, bar) =>
{
Assert.IsTrue(bar.EndTime == time);
consolidatedBarsCount++;
};
PushBarsThrough(1000, Time.OneHour, consolidator, ref time);
// Scan one last time so we can emit the 1000th bar
consolidator.Scan(time);
Assert.AreEqual(500, consolidatedBarsCount);
}
[TestCase(14)] // 2PM
[TestCase(15)] // 3PM
[TestCase(16)] // 4PM
public void BarsEmitOnTime(int hour)
{
// This test just pushes one full hourly bar into a consolidator
// and scans to see if it will emit immediately as expected
using var consolidator = new TradeBarConsolidator(Time.OneHour);
var consolidatedBarsCount = 0;
TradeBar latestBar = null;
var time = new DateTime(2015, 04, 13, hour, 0, 0);
consolidator.DataConsolidated += (sender, bar) =>
{
latestBar = bar;
consolidatedBarsCount++;
};
// Update with one tradebar that ends at this time
// This is to simulate getting a data bar for the last period
consolidator.Update(new TradeBar { Time = time.Subtract(Time.OneMinute), Period = Time.OneMinute });
// Assert that the bar hasn't emitted
Assert.IsNull(latestBar);
Assert.AreEqual(0, consolidatedBarsCount);
// Scan afterwards (Like algorithmManager does)
consolidator.Scan(time);
// Assert that the bar emitted
Assert.IsNotNull(latestBar);
Assert.IsTrue(latestBar.EndTime == time);
Assert.AreEqual(1, consolidatedBarsCount);
}
[TestCase(typeof(BaseDataConsolidator))]
[TestCase(typeof(TradeBarConsolidator))]
[TestCase(typeof(QuoteBarConsolidator))]
[TestCase(typeof(TickConsolidator))]
[TestCase(typeof(TickQuoteBarConsolidator))]
[TestCase(typeof(OpenInterestConsolidator))]
[TestCase(typeof(DynamicDataConsolidator))]
public void ConsolidatorShouldConsolidateOnMaxCountAndUseLastEndTime(Type consolidatorType)
{
// Create a consolidator with maxCount = 2
var consolidator = (IDataConsolidator)Activator.CreateInstance(consolidatorType, 2);
IBaseData consolidated = null;
consolidator.DataConsolidated += (sender, bar) =>
{
// Store the consolidated bar when the DataConsolidated event fires
consolidated = bar;
};
var startDate = new DateTime(2015, 04, 13, 10, 20, 0);
var expectedEndTime = startDate.AddMinutes(61);
var tickType =
consolidatorType == typeof(TickQuoteBarConsolidator) ? TickType.Quote :
consolidatorType == typeof(TickConsolidator) ? TickType.Trade :
TickType.OpenInterest;
var tradeBars = new List<TradeBar>
{
new TradeBar { Symbol = Symbols.SPY, DataType = MarketDataType.TradeBar, Time = startDate, EndTime = startDate.AddMinutes(1) },
new TradeBar { Symbol = Symbols.SPY, DataType = MarketDataType.TradeBar, Time = startDate.AddMinutes(1), EndTime = startDate.AddMinutes(2) },
new TradeBar { Symbol = Symbols.SPY, DataType = MarketDataType.TradeBar, Time = startDate.AddMinutes(2), EndTime = startDate.AddMinutes(3) },
new TradeBar { Symbol = Symbols.SPY, DataType = MarketDataType.TradeBar, Time = startDate.AddHours(1), EndTime = startDate.AddMinutes(61) },
};
var quoteBars = new List<QuoteBar>
{
new QuoteBar { Symbol = Symbols.SPY, DataType = MarketDataType.QuoteBar, Time = startDate, EndTime = startDate.AddMinutes(1) },
new QuoteBar { Symbol = Symbols.SPY, DataType = MarketDataType.QuoteBar, Time = startDate.AddMinutes(1), EndTime = startDate.AddMinutes(2) },
new QuoteBar { Symbol = Symbols.SPY, DataType = MarketDataType.QuoteBar, Time = startDate.AddMinutes(2), EndTime = startDate.AddMinutes(3) },
new QuoteBar { Symbol = Symbols.SPY, DataType = MarketDataType.QuoteBar, Time = startDate.AddHours(1), EndTime = startDate.AddMinutes(61) },
};
var ticks = new List<Tick>
{
new Tick { Symbol = Symbols.SPY, DataType = MarketDataType.Tick, TickType = tickType, Time = startDate, EndTime = startDate.AddMinutes(1) },
new Tick { Symbol = Symbols.SPY, DataType = MarketDataType.Tick, TickType = tickType, Time = startDate.AddMinutes(1), EndTime = startDate.AddMinutes(2) },
new Tick { Symbol = Symbols.SPY, DataType = MarketDataType.Tick, TickType = tickType, Time = startDate.AddMinutes(2), EndTime = startDate.AddMinutes(3) },
new Tick { Symbol = Symbols.SPY, DataType = MarketDataType.Tick, TickType = tickType, Time = startDate.AddHours(1), EndTime = startDate.AddMinutes(61) },
};
var customData = new List<CustomData>
{
new CustomData { Symbol = Symbols.SPY, Time = startDate, EndTime = startDate.AddMinutes(1) },
new CustomData { Symbol = Symbols.SPY, Time = startDate.AddMinutes(1), EndTime = startDate.AddMinutes(2) },
new CustomData { Symbol = Symbols.SPY, Time = startDate.AddMinutes(2), EndTime = startDate.AddMinutes(3) },
new CustomData { Symbol = Symbols.SPY, Time = startDate.AddHours(1), EndTime = startDate.AddMinutes(61) },
};
var dataMap = new Dictionary<Type, IEnumerable<BaseData>>
{
{ typeof(TradeBarConsolidator), tradeBars },
{ typeof(BaseDataConsolidator), tradeBars },
{ typeof(QuoteBarConsolidator), quoteBars },
{ typeof(TickQuoteBarConsolidator), ticks },
{ typeof(OpenInterestConsolidator), ticks },
{ typeof(TickConsolidator), ticks },
{ typeof(DynamicDataConsolidator), customData }
};
if (dataMap.TryGetValue(consolidatorType, out var dataList))
{
// Feed the consolidator with the appropriate data
foreach (var data in dataList)
{
consolidator.Update(data);
}
}
// Assert the consolidated bar is not null and its EndTime matches the last received bar's EndTime
Assert.IsNotNull(consolidated);
Assert.AreEqual(Symbols.SPY, consolidated.Symbol);
Assert.AreEqual(expectedEndTime, consolidated.EndTime);
}
private static void PushBarsThrough(int barCount, TimeSpan period, TradeBarConsolidator consolidator, ref DateTime time)
{
TradeBar bar;
for (int i = 0; i < barCount; i++)
{
bar = new TradeBar { Time = time, Period = period };
consolidator.Update(bar);
// Advance time
time += period;
}
}
private class CustomData : DynamicData
{
public override BaseData Reader(SubscriptionDataConfig config, string line, DateTime date, bool isLiveMode)
{
throw new NotImplementedException();
}
public override SubscriptionDataSource GetSource(SubscriptionDataConfig config, DateTime date, bool isLiveMode)
{
throw new NotImplementedException();
}
}
}
}