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

592 lines
22 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]
public class TradeBarConsolidatorTests: BaseConsolidatorTests
{
[Test]
public void ZeroCountAlwaysFires()
{
// defining a TradeBarConsolidator with a zero max count should cause it to always fire identity
TradeBar consolidated = null;
using var consolidator = new TradeBarConsolidator(0);
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
consolidator.Update(new TradeBar());
Assert.IsNotNull(consolidated);
}
[Test]
public void OneCountAlwaysFires()
{
// defining a TradeBarConsolidator with a one max count should cause it to always fire identity
TradeBar consolidated = null;
using var consolidator = new TradeBarConsolidator(1);
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
consolidator.Update(new TradeBar());
Assert.IsNotNull(consolidated);
}
[Test]
public void TwoCountFiresEveryOther()
{
// defining a TradeBarConsolidator with a two max count should cause it to fire every other TradeBar
TradeBar consolidated = null;
using var consolidator = new TradeBarConsolidator(2);
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
consolidator.Update(new TradeBar());
Assert.IsNull(consolidated);
consolidator.Update(new TradeBar());
Assert.IsNotNull(consolidated);
consolidated = null;
consolidator.Update(new TradeBar());
Assert.IsNull(consolidated);
consolidator.Update(new TradeBar());
Assert.IsNotNull(consolidated);
}
[Test]
public void ZeroSpanAlwaysThrows()
{
// defining a TradeBarConsolidator with a zero period should cause it to always throw an exception
TradeBar consolidated = null;
using var consolidator = new TradeBarConsolidator(TimeSpan.Zero);
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
var reference = new DateTime(2014, 12, 01, 01, 01, 00);
Assert.Throws<ArgumentException>(() => consolidator.Update(new TradeBar { Time = reference, Period = Time.OneDay }));
}
[Test]
public void ConsolidatesOHLCV()
{
// verifies that the TradeBarConsolidator correctly consolidates OHLCV data into a new TradeBar instance
TradeBar consolidated = null;
using var consolidator = new TradeBarConsolidator(3);
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
var tb1 = new TradeBar
{
Symbol = Symbols.SPY,
Open = 10,
High = 100,
Low = 1,
Close = 50,
Volume = 75,
DataType = MarketDataType.TradeBar
};
var tb2 = new TradeBar
{
Symbol = Symbols.SPY,
Open = 50,
High = 123,
Low = 35,
Close = 75,
Volume = 100,
DataType = MarketDataType.TradeBar
};
var tb3 = new TradeBar
{
Symbol = Symbols.SPY,
Open = 75,
High = 100,
Low = 50,
Close = 83,
Volume = 125,
DataType = MarketDataType.TradeBar
};
consolidator.Update(tb1);
consolidator.Update(tb2);
consolidator.Update(tb3);
Assert.IsNotNull(consolidated);
Assert.AreEqual(Symbols.SPY, consolidated.Symbol);
Assert.AreEqual(10m, consolidated.Open);
Assert.AreEqual(123m, consolidated.High);
Assert.AreEqual(1m, consolidated.Low);
Assert.AreEqual(83m, consolidated.Close);
Assert.AreEqual(300L, consolidated.Volume);
}
[Test]
public void DoesNotConsolidateDifferentSymbols()
{
// verifies that the TradeBarConsolidator does not consolidate data with different symbols
using var consolidator = new TradeBarConsolidator(2);
var tb1 = new TradeBar
{
Symbol = Symbols.AAPL,
Open = 10,
High = 100,
Low = 1,
Close = 50,
Volume = 75,
DataType = MarketDataType.TradeBar
};
var tb2 = new TradeBar
{
Symbol = Symbols.ZNGA,
Open = 50,
High = 123,
Low = 35,
Close = 75,
Volume = 100,
DataType = MarketDataType.TradeBar
};
consolidator.Update(tb1);
Exception ex = Assert.Throws<InvalidOperationException>(() => consolidator.Update(tb2));
Assert.IsTrue(ex.Message.Contains("is not the same", StringComparison.InvariantCultureIgnoreCase));
}
[Test]
public void ConsolidatedTimeIsFromBeginningOfBar()
{
// verifies that the consolidated bar uses the time from the beginning of the first bar
// in the period that covers the current bar
using var consolidator = new TradeBarConsolidator(TimeSpan.FromMinutes(2));
TradeBar consolidated = null;
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
var reference = new DateTime(2014, 12, 1, 10, 00, 0);
//10:00 - start new
consolidator.Update(new TradeBar {Time = reference});
Assert.IsNull(consolidated);
//10:01 - aggregate
consolidator.Update(new TradeBar {Time = reference.AddMinutes(1)});
Assert.IsNull(consolidated);
//10:02 - fire & start new
consolidator.Update(new TradeBar {Time = reference.AddMinutes(2)});
Assert.IsNotNull(consolidated);
Assert.AreEqual(reference, consolidated.Time);
consolidated = null;
//10:03 - aggregate
consolidator.Update(new TradeBar {Time = reference.AddMinutes(3)});
Assert.IsNull(consolidated);
//10:05 - fire & start new
consolidator.Update(new TradeBar {Time = reference.AddMinutes(5)});
Assert.IsNotNull(consolidated);
Assert.AreEqual(reference.AddMinutes(2), consolidated.Time);
consolidated = null;
//10:08 - fire & start new
consolidator.Update(new TradeBar {Time = reference.AddMinutes(8)});
Assert.IsNotNull(consolidated);
Assert.AreEqual(reference.AddMinutes(4), consolidated.Time);
consolidated = null;
//10:08:01 - aggregate
consolidator.Update(new TradeBar {Time = reference.AddMinutes(8).AddSeconds(1)});
Assert.IsNull(consolidated);
//10:09 - aggregate
consolidator.Update(new TradeBar {Time = reference.AddMinutes(9)});
Assert.IsNull(consolidated);
}
[Test]
public void HandlesDataGapsInMixedMode()
{
// define a three minute consolidator on a one minute stream of data
using var consolidator = new TradeBarConsolidator(3, TimeSpan.FromMinutes(3));
TradeBar consolidated = null;
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
var reference = new DateTime(2014, 12, 1, 10, 00, 0);
//10:00 - new
consolidator.Update(new TradeBar {Time = reference});
Assert.IsNull(consolidated);
//10:01 - aggregate
consolidator.Update(new TradeBar {Time = reference.AddMinutes(1)});
Assert.IsNull(consolidated);
//10:02 - fire
consolidator.Update(new TradeBar {Time = reference.AddMinutes(2)});
Assert.IsNotNull(consolidated);
Assert.AreEqual(reference, consolidated.Time);
//10:03 - new
consolidator.Update(new TradeBar {Time = reference.AddMinutes(3)});
Assert.AreEqual(reference, consolidated.Time);
//10:06 - aggregate/fire
consolidator.Update(new TradeBar {Time = reference.AddMinutes(6)});
Assert.AreEqual(reference.AddMinutes(3), consolidated.Time);
//10:08 - new/fire -- will have timestamp from 10:08, instead of 10:06
consolidator.Update(new TradeBar {Time = reference.AddMinutes(8)});
Assert.AreEqual(reference.AddMinutes(8), consolidated.Time);
}
[Test]
public void HandlesGappingAcrossDays()
{
// this test requires inspection to verify we're getting clean bars on the correct times
using var consolidator = new TradeBarConsolidator(TimeSpan.FromHours(1));
TradeBar consolidated = null;
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
// from 1/1 9:30 to 1/2 12:00 by minute
var start = new DateTime(2014, 01, 01, 09, 30, 00, 00);
var end = new DateTime(2014, 01, 02, 12, 00, 00, 00);
foreach (var bar in StreamTradeBars(start, end, TimeSpan.FromMinutes(1)))
{
consolidator.Update(bar);
}
}
/// <summary>
/// Testing the behaviors where, the bar range is closed on the left and open on
/// the right in time span mode: [T, T+TimeSpan).
/// For example, if time span is 1 minute, we have [10:00, 10:01): so data at
/// 10:01 is not included in the bar starting at 10:00.
/// </summary>
[Test]
public void ClosedLeftOpenRightInTimeSpanModeTest()
{
// define a three minute consolidator
int timeSpanUnits = 3;
using var consolidator = new TradeBarConsolidator(TimeSpan.FromMinutes(timeSpanUnits));
TradeBar consolidated = null;
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
var refDateTime = new DateTime(2014, 12, 1, 10, 00, 0);
// loop for 3 times the timeSpanUnits + 1, so it would consolidate the bars 3 times
for (int i=0; i < 3*timeSpanUnits + 1 ; ++i)
{
consolidator.Update(new TradeBar { Time = refDateTime });
if (i < timeSpanUnits) // before initial consolidation happens
{
Assert.IsNull(consolidated);
}
else
{
Assert.IsNotNull(consolidated);
if (i % timeSpanUnits == 0) // i = 3, 6, 9
{
Assert.AreEqual(refDateTime.AddMinutes(-timeSpanUnits), consolidated.Time);
}
}
refDateTime = refDateTime.AddMinutes(1);
}
}
[Test]
public void AggregatesPeriodInCountModeWithDailyData()
{
TradeBar consolidated = null;
var period = TimeSpan.FromDays(1);
using var consolidator = new TradeBarConsolidator(2);
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
var reference = new DateTime(2015, 04, 13);
consolidator.Update(new TradeBar { Time = reference, Period = period});
Assert.IsNull(consolidated);
consolidator.Update(new TradeBar { Time = reference.AddDays(1), Period = period });
Assert.IsNotNull(consolidated);
Assert.AreEqual(TimeSpan.FromDays(2), consolidated.Period);
consolidated = null;
consolidator.Update(new TradeBar { Time = reference.AddDays(2), Period = period });
Assert.IsNull(consolidated);
consolidator.Update(new TradeBar { Time = reference.AddDays(3), Period = period });
Assert.IsNotNull(consolidated);
Assert.AreEqual(TimeSpan.FromDays(2), consolidated.Period);
}
[Test]
public void AggregatesPeriodInPeriodModeWithDailyData()
{
TradeBar consolidated = null;
var period = TimeSpan.FromDays(2);
using var consolidator = new TradeBarConsolidator(period);
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
var reference = new DateTime(2015, 04, 13);
consolidator.Update(new TradeBar { Time = reference, Period = Time.OneDay});
Assert.IsNull(consolidated);
consolidator.Update(new TradeBar { Time = reference.AddDays(1), Period = Time.OneDay });
Assert.IsNull(consolidated);
consolidator.Update(new TradeBar { Time = reference.AddDays(2), Period = Time.OneDay });
Assert.IsNotNull(consolidated);
Assert.AreEqual(period, consolidated.Period);
consolidated = null;
consolidator.Update(new TradeBar { Time = reference.AddDays(3), Period = Time.OneDay });
Assert.IsNull(consolidated);
consolidator.Update(new TradeBar { Time = reference.AddDays(4), Period = Time.OneDay });
Assert.IsNotNull(consolidated);
Assert.AreEqual(period, consolidated.Period);
}
[Test]
public void ThrowsWhenPeriodIsSmallerThanDataPeriod()
{
TradeBar consolidated = null;
using var consolidator = new TradeBarConsolidator(Time.OneHour);
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
var reference = new DateTime(2015, 04, 13);
Assert.Throws<ArgumentException>(() => consolidator.Update(new TradeBar { Time = reference, Period = Time.OneDay }));
}
[Test]
public void GentlyHandlesPeriodAndDataAreSameResolution()
{
TradeBar consolidated = null;
using var consolidator = new TradeBarConsolidator(Time.OneDay);
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
var reference = new DateTime(2015, 04, 13);
var bar = new TradeBar { Time = reference, Period = Time.OneDay };
consolidator.Update(bar);
Assert.IsNull(consolidated);
consolidator.Scan(bar.EndTime);
Assert.IsNotNull(consolidated);
Assert.IsNotNull(consolidated);
Assert.AreEqual(reference, consolidated.Time);
Assert.AreEqual(Time.OneDay, consolidated.Period);
}
[Test]
public void FiresEventAfterTimePassesViaScan()
{
TradeBar consolidated = null;
var period = TimeSpan.FromDays(2);
using var consolidator = new TradeBarConsolidator(period);
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
var reference = new DateTime(2015, 04, 13);
consolidator.Update(new TradeBar { Time = reference, Period = Time.OneDay });
Assert.IsNull(consolidated);
consolidator.Scan(reference + period);
Assert.IsNotNull(consolidated);
Assert.AreEqual(reference, consolidated.Time);
}
[Test]
public void ConsolidatedPeriodEqualsTimeBasedConsolidatorPeriod()
{
TradeBar consolidated = null;
var period = TimeSpan.FromMinutes(2);
using var consolidator = new TradeBarConsolidator(period);
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
var reference = new DateTime(2015, 04, 13, 10, 20, 0);
var time = reference;
consolidator.Update(new TradeBar { Time = time, Period = Time.OneMinute });
time = reference.Add(period);
consolidator.Scan(time);
Assert.IsNotNull(consolidated);
Assert.AreEqual(reference, consolidated.Time);
Assert.AreEqual(period, consolidated.Period);
}
[Test]
public void FiresEventAfterTimePassesViaScanWithMultipleResolutions()
{
TradeBar consolidated = null;
var period = TimeSpan.FromMinutes(2);
using var consolidator = new TradeBarConsolidator(period);
consolidator.DataConsolidated += (sender, bar) =>
{
consolidated = bar;
};
var reference = new DateTime(2015, 04, 13, 10, 20, 0);
var time = reference;
for (int i = 0; i < 10; i++)
{
consolidator.Update(new TradeBar {Time = time, Period = Time.OneSecond});
time = time.AddSeconds(1);
consolidator.Scan(time);
Assert.IsNull(consolidated);
}
consolidator.Update(new TradeBar { Time = time, Period = Time.OneMinute });
time = reference.Add(period);
consolidator.Scan(time);
Assert.IsNotNull(consolidated);
Assert.AreEqual(reference, consolidated.Time);
Assert.AreEqual(period, consolidated.Period);
consolidated = null;
consolidator.Update(new TradeBar { Time = time, Period = Time.OneSecond });
time = time.AddSeconds(1);
consolidator.Scan(time);
Assert.IsNull(consolidated);
time = time.AddSeconds(-1);
consolidator.Update(new TradeBar { Time = time, Period = Time.OneMinute });
time = time.AddMinutes(1);
consolidator.Scan(time);
Assert.IsNull(consolidated);
consolidator.Update(new TradeBar { Time = time, Period = Time.OneMinute });
time = time.AddMinutes(1);
consolidator.Scan(time);
Assert.IsNotNull(consolidated);
Assert.AreEqual(reference.AddMinutes(2), consolidated.Time);
Assert.AreEqual(period, consolidated.Period);
}
private readonly TimeSpan marketStop = new DateTime(2000, 1, 1, 12 + 4, 0, 0).TimeOfDay;
private readonly TimeSpan marketStart = new DateTime(2000, 1, 1, 9, 30, 0).TimeOfDay;
private IEnumerable<TradeBar> StreamTradeBars(DateTime start, DateTime end, TimeSpan resolution, bool skipAferMarketHours = true)
{
DateTime current = start;
while (current < end)
{
var timeOfDay = current.TimeOfDay;
if (skipAferMarketHours && (marketStart > timeOfDay || marketStop < timeOfDay))
{
// set current to the next days market start
current = current.Date.AddDays(1).Add(marketStart);
continue;
}
// either we don't care about after market hours or it's within regular market hours
yield return new TradeBar {Time = current};
current = current + resolution;
}
}
protected override IDataConsolidator CreateConsolidator()
{
return new TradeBarConsolidator(2);
}
protected override IEnumerable<IBaseData> GetTestValues()
{
var time = new DateTime(2015, 04, 13, 8, 31, 0);
return new List<TradeBar>()
{
new TradeBar(){ Time = time, Period = Time.OneMinute, Symbol = Symbols.SPY, High = 10 },
new TradeBar(){ Time = time.AddMinutes(1), Period = Time.OneMinute, Symbol = Symbols.SPY, High = 12 },
new TradeBar(){ Time = time.AddMinutes(2), Period = Time.OneMinute, Symbol = Symbols.SPY, High = 10 },
new TradeBar(){ Time = time.AddMinutes(3), Period = Time.OneMinute, Symbol = Symbols.SPY, High = 5 },
new TradeBar(){ Time = time.AddMinutes(4), Period = Time.OneMinute, Symbol = Symbols.SPY, High = 15 },
new TradeBar(){ Time = time.AddMinutes(5), Period = Time.OneMinute, Symbol = Symbols.SPY, High = 20 },
new TradeBar(){ Time = time.AddMinutes(6), Period = Time.OneMinute, Symbol = Symbols.SPY, High = 18 },
new TradeBar(){ Time = time.AddMinutes(7), Period = Time.OneMinute, Symbol = Symbols.SPY, High = 12 },
new TradeBar(){ Time = time.AddMinutes(8), Period = Time.OneMinute, Symbol = Symbols.SPY, High = 25 },
new TradeBar(){ Time = time.AddMinutes(9), Period = Time.OneMinute, Symbol = Symbols.SPY, High = 30 },
new TradeBar(){ Time = time.AddMinutes(10), Period = Time.OneMinute, Symbol = Symbols.SPY, High = 26 },
};
}
}
}