Skip to content

Commit 81b2d45

Browse files
Add Consolidator Optional Start time (#9034)
- Add consolidators optional start time. Adding tests
1 parent fda4859 commit 81b2d45

File tree

10 files changed

+267
-17
lines changed

10 files changed

+267
-17
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using QuantConnect.Data;
18+
using QuantConnect.Interfaces;
19+
using QuantConnect.Data.Market;
20+
using System.Collections.Generic;
21+
using QuantConnect.Data.Consolidators;
22+
23+
namespace QuantConnect.Algorithm.CSharp
24+
{
25+
/// <summary>
26+
/// Regression algorithm show casing and asserting the behavior of creating a consolidator specifying the start time
27+
/// </summary>
28+
public class ConsolidatorStartTimeRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
29+
{
30+
private readonly Queue<TimeSpan> _expectedConsolidationTime = new([
31+
new TimeSpan(9, 30, 0),
32+
new TimeSpan(10, 30, 0),
33+
new TimeSpan(11, 30, 0),
34+
new TimeSpan(12, 30, 0),
35+
new TimeSpan(13, 30, 0),
36+
new TimeSpan(14, 30, 0)
37+
]);
38+
private TradeBarConsolidator consolidator;
39+
40+
/// <summary>
41+
/// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.
42+
/// </summary>
43+
public override void Initialize()
44+
{
45+
SetStartDate(2013, 10, 04);
46+
SetEndDate(2013, 10, 04);
47+
48+
AddEquity("SPY", Resolution.Minute);
49+
50+
consolidator = new TradeBarConsolidator(TimeSpan.FromHours(1), startTime: new TimeSpan(9, 30, 0));
51+
consolidator.DataConsolidated += BarHandler;
52+
53+
SubscriptionManager.AddConsolidator("SPY", consolidator);
54+
}
55+
56+
private void BarHandler(object _, TradeBar bar)
57+
{
58+
if (Time != bar.EndTime)
59+
{
60+
throw new RegressionTestException($"Unexpected consolidation time {bar.Time} != {Time}!");
61+
}
62+
63+
var expected = _expectedConsolidationTime.Dequeue();
64+
if (bar.Time.TimeOfDay != expected)
65+
{
66+
throw new RegressionTestException($"Unexpected consolidation time {bar.Time.TimeOfDay} != {expected}!");
67+
}
68+
69+
if (bar.Period != TimeSpan.FromHours(1))
70+
{
71+
throw new RegressionTestException($"Unexpected consolidation period {bar.Period}!");
72+
}
73+
}
74+
75+
public override void OnEndOfAlgorithm()
76+
{
77+
if (_expectedConsolidationTime.Count > 0)
78+
{
79+
throw new RegressionTestException("Unexpected consolidation times!");
80+
}
81+
}
82+
83+
/// <summary>
84+
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
85+
/// </summary>
86+
public bool CanRunLocally { get; } = true;
87+
88+
/// <summary>
89+
/// This is used by the regression test system to indicate which languages this algorithm is written in.
90+
/// </summary>
91+
public List<Language> Languages { get; } = new() { Language.CSharp, Language.Python };
92+
93+
/// <summary>
94+
/// Data Points count of all timeslices of algorithm
95+
/// </summary>
96+
public long DataPoints => 795;
97+
98+
/// <summary>
99+
/// Data Points count of the algorithm history
100+
/// </summary>
101+
public int AlgorithmHistoryDataPoints => 0;
102+
103+
/// <summary>
104+
/// Final status of the algorithm
105+
/// </summary>
106+
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
107+
108+
/// <summary>
109+
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
110+
/// </summary>
111+
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
112+
{
113+
{"Total Orders", "0"},
114+
{"Average Win", "0%"},
115+
{"Average Loss", "0%"},
116+
{"Compounding Annual Return", "0%"},
117+
{"Drawdown", "0%"},
118+
{"Expectancy", "0"},
119+
{"Start Equity", "100000"},
120+
{"End Equity", "100000"},
121+
{"Net Profit", "0%"},
122+
{"Sharpe Ratio", "0"},
123+
{"Sortino Ratio", "0"},
124+
{"Probabilistic Sharpe Ratio", "0%"},
125+
{"Loss Rate", "0%"},
126+
{"Win Rate", "0%"},
127+
{"Profit-Loss Ratio", "0"},
128+
{"Alpha", "0"},
129+
{"Beta", "0"},
130+
{"Annual Standard Deviation", "0"},
131+
{"Annual Variance", "0"},
132+
{"Information Ratio", "0"},
133+
{"Tracking Error", "0"},
134+
{"Treynor Ratio", "0"},
135+
{"Total Fees", "$0.00"},
136+
{"Estimated Strategy Capacity", "$0"},
137+
{"Lowest Capacity Asset", ""},
138+
{"Portfolio Turnover", "0%"},
139+
{"Drawdown Recovery", "0"},
140+
{"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
141+
};
142+
}
143+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
2+
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
from AlgorithmImports import *
15+
from collections import deque
16+
17+
### <summary>
18+
### Regression algorithm show casing and asserting the behavior of creating a consolidator specifying the start time
19+
### </summary>
20+
class ConsolidatorStartTimeRegressionAlgorithm(QCAlgorithm):
21+
def initialize(self):
22+
self.set_start_date(2013, 10, 4)
23+
self.set_end_date(2013, 10, 4)
24+
25+
self.add_equity("SPY", Resolution.MINUTE)
26+
27+
consolidator = TradeBarConsolidator(timedelta(hours=1), start_time=timedelta(hours=9, minutes=30))
28+
consolidator.data_consolidated += self.bar_handler
29+
self.subscription_manager.add_consolidator("SPY", consolidator)
30+
31+
self._expectedConsolidationTime = deque()
32+
self._expectedConsolidationTime.append(time(14, 30))
33+
self._expectedConsolidationTime.append(time(13, 30))
34+
self._expectedConsolidationTime.append(time(12, 30))
35+
self._expectedConsolidationTime.append(time(11, 30))
36+
self._expectedConsolidationTime.append(time(10, 30))
37+
self._expectedConsolidationTime.append(time(9, 30))
38+
39+
def bar_handler(self, _, bar):
40+
if self.time != bar.end_time:
41+
raise RegressionTestException(f"Unexpected consolidation time {bar.Time} != {Time}!")
42+
43+
self.debug(f"{self.Time} - Consolidation")
44+
expected = self._expectedConsolidationTime.pop()
45+
if bar.time.time() != expected:
46+
raise RegressionTestException(f"Unexpected consolidation time {bar.time.time()} != {expected}!")
47+
48+
if bar.period != timedelta(hours=1):
49+
raise RegressionTestException(f"Unexpected consolidation period {bar.period}!")
50+
51+
def on_end_of_algorithm(self):
52+
if len(self._expectedConsolidationTime) > 0:
53+
raise RegressionTestException("Unexpected consolidation times!")

Common/Data/Consolidators/OpenInterestConsolidator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ public static OpenInterestConsolidator FromResolution(Resolution resolution)
4242
/// Creates a consolidator to produce a new 'OpenInterest' representing the period
4343
/// </summary>
4444
/// <param name="period">The minimum span of time before emitting a consolidated bar</param>
45-
public OpenInterestConsolidator(TimeSpan period)
46-
: base(period)
45+
/// <param name="startTime">Optionally the bar start time anchor to use</param>
46+
public OpenInterestConsolidator(TimeSpan period, TimeSpan? startTime = null)
47+
: base(period, startTime)
4748
{
4849
_hourOrDailyConsolidation = period >= Time.OneHour;
4950
}

Common/Data/Consolidators/PeriodCountConsolidatorBase.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
*/
1616

1717
using System;
18-
using System.Runtime.CompilerServices;
19-
using QuantConnect.Data.Market;
2018
using Python.Runtime;
19+
using QuantConnect.Util;
20+
using QuantConnect.Data.Market;
21+
using System.Runtime.CompilerServices;
2122

2223
namespace QuantConnect.Data.Consolidators
2324
{
@@ -58,8 +59,9 @@ private PeriodCountConsolidatorBase(IPeriodSpecification periodSpecification)
5859
/// Creates a consolidator to produce a new <typeparamref name="TConsolidated"/> instance representing the period
5960
/// </summary>
6061
/// <param name="period">The minimum span of time before emitting a consolidated bar</param>
61-
protected PeriodCountConsolidatorBase(TimeSpan period)
62-
: this(new TimeSpanPeriodSpecification(period))
62+
/// <param name="startTime">Optionally the bar start time anchor to use</param>
63+
protected PeriodCountConsolidatorBase(TimeSpan period, TimeSpan? startTime = null)
64+
: this(new TimeSpanPeriodSpecification(period, startTime))
6365
{
6466
_period = _periodSpecification.Period;
6567
}
@@ -420,17 +422,25 @@ public MixedModePeriodSpecification(TimeSpan period)
420422
/// </summary>
421423
private class TimeSpanPeriodSpecification : IPeriodSpecification
422424
{
425+
public TimeSpan? StartTime { get; }
423426
public TimeSpan? Period { get; }
424427

425-
public TimeSpanPeriodSpecification(TimeSpan period)
428+
public TimeSpanPeriodSpecification(TimeSpan period, TimeSpan? startTime = null)
426429
{
427430
Period = period;
431+
StartTime = startTime;
428432
}
429433

430-
public DateTime GetRoundedBarTime(DateTime time) =>
431-
Period.Value > Time.OneDay
434+
public DateTime GetRoundedBarTime(DateTime time)
435+
{
436+
if (StartTime.HasValue)
437+
{
438+
return LeanData.GetConsolidatorStartTime(Period.Value, StartTime.Value, time);
439+
}
440+
return Period.Value > Time.OneDay
432441
? time // #4915 For periods larger than a day, don't use a rounding schedule.
433442
: time.RoundDown(Period.Value);
443+
}
434444
}
435445

436446
/// <summary>

Common/Data/Consolidators/QuoteBarConsolidator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ public class QuoteBarConsolidator : PeriodCountConsolidatorBase<QuoteBar, QuoteB
2929
/// Initializes a new instance of the <see cref="QuoteBarConsolidator"/> class
3030
/// </summary>
3131
/// <param name="period">The minimum span of time before emitting a consolidated bar</param>
32-
public QuoteBarConsolidator(TimeSpan period)
33-
: base(period)
32+
/// <param name="startTime">Optionally the bar start time anchor to use</param>
33+
public QuoteBarConsolidator(TimeSpan period, TimeSpan? startTime = null)
34+
: base(period, startTime)
3435
{
3536
}
3637

Common/Data/Consolidators/TickQuoteBarConsolidator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ public class TickQuoteBarConsolidator : PeriodCountConsolidatorBase<Tick, QuoteB
2929
/// Initializes a new instance of the <see cref="TickQuoteBarConsolidator"/> class
3030
/// </summary>
3131
/// <param name="period">The minimum span of time before emitting a consolidated bar</param>
32-
public TickQuoteBarConsolidator(TimeSpan period)
33-
: base(period)
32+
/// <param name="startTime">Optionally the bar start time anchor to use</param>
33+
public TickQuoteBarConsolidator(TimeSpan period, TimeSpan? startTime = null)
34+
: base(period, startTime)
3435
{
3536
}
3637

Common/Data/Consolidators/TradeBarConsolidator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ public static TradeBarConsolidator FromResolution(Resolution resolution)
4343
/// Creates a consolidator to produce a new 'TradeBar' representing the period
4444
/// </summary>
4545
/// <param name="period">The minimum span of time before emitting a consolidated bar</param>
46-
public TradeBarConsolidator(TimeSpan period)
47-
: base(period)
46+
/// <param name="startTime">Optionally the bar start time anchor to use</param>
47+
public TradeBarConsolidator(TimeSpan period, TimeSpan? startTime = null)
48+
: base(period, startTime)
4849
{
4950
}
5051

Common/Data/Consolidators/TradeBarConsolidatorBase.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ public abstract class TradeBarConsolidatorBase<T> : PeriodCountConsolidatorBase<
3333
/// Creates a consolidator to produce a new 'TradeBar' representing the period
3434
/// </summary>
3535
/// <param name="period">The minimum span of time before emitting a consolidated bar</param>
36-
protected TradeBarConsolidatorBase(TimeSpan period)
37-
: base(period)
36+
/// <param name="startTime">Optionally the bar start time anchor to use</param>
37+
protected TradeBarConsolidatorBase(TimeSpan period, TimeSpan? startTime = null)
38+
: base(period, startTime)
3839
{
3940
}
4041

Common/Util/LeanData.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,24 @@ public static IEnumerable<TradeBar> AggregateTicksToTradeBars(IEnumerable<Tick>
14401440
return Aggregate(new TickConsolidator(resolution), ticks, symbol);
14411441
}
14421442

1443+
/// <summary>
1444+
/// Helper method to calculate the start time of a consolidator bar given a period, and anchor start time and the current data time
1445+
/// </summary>
1446+
public static DateTime GetConsolidatorStartTime(TimeSpan period, TimeSpan startTime, DateTime time)
1447+
{
1448+
var referenceStart = time.Date + startTime;
1449+
if (period >= TimeSpan.FromDays(7))
1450+
{
1451+
// anchor to start of the month
1452+
referenceStart = new DateTime(time.Year, time.Month, 1) + startTime;
1453+
}
1454+
1455+
var difference = time - referenceStart;
1456+
1457+
var intervalsPassed = Math.Floor(difference.TotalSeconds / period.TotalSeconds);
1458+
return referenceStart + TimeSpan.FromSeconds(intervalsPassed * period.TotalSeconds);
1459+
}
1460+
14431461
/// <summary>
14441462
/// Helper method to return the start time and period of a bar the given point time should be part of
14451463
/// </summary>

Tests/Common/Util/LeanDataTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,27 @@ public void TearDown()
4646
SymbolCache.Clear();
4747
}
4848

49+
[TestCase("0.00:04:00", "0.09:30:00", "20251013 09:29:01", "20251013 09:26:00")]
50+
[TestCase("0.00:04:00", "0.09:30:00", "20251013 09:30:01", "20251013 09:30:00")]
51+
[TestCase("0.00:04:00", "0.09:30:00", "20251013 09:35:00", "20251013 09:34:00")]
52+
[TestCase("0.00:05:00", "0.09:00:00", "20251013 09:35:00", "20251013 09:35:00")]
53+
54+
[TestCase("0.02:00:00", "0.09:30:00", "20251013 11:30:01", "20251013 11:30:00")]
55+
[TestCase("0.02:00:00", "0.09:30:00", "20251013 09:30:01", "20251013 09:30:00")]
56+
[TestCase("0.02:00:00", "0.09:30:00", "20251013 08:30:01", "20251013 07:30:00")]
57+
58+
[TestCase("10.00:00:00", "0.00:00:00", "20251013 11:30:01", "20251011 00:00:00")]
59+
[TestCase("10.00:00:00", "0.00:00:00", "20251007 11:30:01", "20251001 00:00:00")]
60+
public void ConsolidatorStarTime(string periodStr, string startTimeStr, string timeStr, string expected)
61+
{
62+
var result = LeanData.GetConsolidatorStartTime(
63+
TimeSpan.ParseExact(periodStr, "d\\.hh\\:mm\\:ss", CultureInfo.InvariantCulture),
64+
TimeSpan.ParseExact(startTimeStr, "d\\.hh\\:mm\\:ss", CultureInfo.InvariantCulture),
65+
DateTime.ParseExact(timeStr, "yyyyMMdd HH\\:mm\\:ss", CultureInfo.InvariantCulture));
66+
67+
Assert.AreEqual(DateTime.ParseExact(expected, "yyyyMMdd HH\\:mm\\:ss", CultureInfo.InvariantCulture), result);
68+
}
69+
4970
[TestCase(16, false, "20240506 09:30", "06:30", false)]
5071
[TestCase(10, false, "20240506 09:30", "06:30", false)]
5172
[TestCase(10, true, "20240506 04:00", "16:00", false)]

0 commit comments

Comments
 (0)