Skip to content

Commit d6acc60

Browse files
authored
Feat: split history request (#10)
* fix: several deprecate href * feat: use max available date by subs * feat: request parameters const names * feat: Execute History Request Parallel * refactor: synchronize request in API class * feat: develop StopWatch Wrapper * remove: RestClient huge Timeout value * fix: GenerateDateRangesWithInterval extension * refactor: ExecuteRequestParallelAsync test:feat: GetHistoryRequestWithLongRange * refactor: ExecuteRequestParallelAsync * refactor: ExecuteRequestParallelAsync if scope * Revert "feat: use max available date by subs" This reverts commit a1819dc. * refactor: use Parallel.ForEachAsync instead of semaphore and task to get history data * remove: NullDisposable * feat: use max available date by subs * feat: request retry if failed in ApiClient * fix: GenerateDateRangesWithInterval refactor: ExecuteRequest * feat: filter of HistoryRequest with ExtendedMarketHours test:feat: amount of bars with different resolution * fix: use Utc time for tick requests remove: extra logs * feat: validate history request on null * remove: not used lib
1 parent ab59b4b commit d6acc60

File tree

10 files changed

+547
-44
lines changed

10 files changed

+547
-44
lines changed

QuantConnect.ThetaData.Tests/TestHelpers.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,11 @@ public static void AssertTradeBars(IEnumerable<TradeBar> tradeBars, Symbol symbo
174174
}
175175

176176
public static HistoryRequest CreateHistoryRequest(Symbol symbol, Resolution resolution, TickType tickType, DateTime startDateTime, DateTime endDateTime,
177-
SecurityExchangeHours exchangeHours = null, DateTimeZone dataTimeZone = null)
177+
SecurityExchangeHours exchangeHours = null, DateTimeZone dataTimeZone = null, bool includeExtendedMarketHours = true)
178178
{
179179
if (exchangeHours == null)
180180
{
181-
exchangeHours = SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork);
181+
exchangeHours = MarketHoursDatabase.FromDataFolder().GetExchangeHours(symbol.ID.Market, symbol, symbol.SecurityType);
182182
}
183183

184184
if (dataTimeZone == null)
@@ -188,15 +188,15 @@ public static HistoryRequest CreateHistoryRequest(Symbol symbol, Resolution reso
188188

189189
var dataType = LeanData.GetDataType(resolution, tickType);
190190
return new HistoryRequest(
191-
startDateTime,
192-
endDateTime,
191+
startDateTime.ConvertToUtc(exchangeHours.TimeZone),
192+
endDateTime.ConvertToUtc(exchangeHours.TimeZone),
193193
dataType,
194194
symbol,
195195
resolution,
196196
exchangeHours,
197197
dataTimeZone,
198-
null,
199-
true,
198+
resolution,
199+
includeExtendedMarketHours,
200200
false,
201201
DataNormalizationMode.Raw,
202202
tickType
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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 NUnit.Framework;
18+
using System.Collections.Generic;
19+
20+
namespace QuantConnect.Lean.DataSource.ThetaData.Tests;
21+
22+
[TestFixture]
23+
public class ThetaDataAdditionalTests
24+
{
25+
[Test]
26+
public void GenerateDateRangesWithNinetyDaysInterval()
27+
{
28+
var intervalDays = 90;
29+
var startDate = new DateTime(2020, 07, 18);
30+
var endDate = new DateTime(2021, 01, 14);
31+
32+
var expectedRanges = new List<(DateTime startDate, DateTime endDate)>
33+
{
34+
(new DateTime(2020, 07, 18), new DateTime(2020, 10, 16)),
35+
(new DateTime(2020, 10, 17), new DateTime(2021, 01, 14)),
36+
};
37+
38+
var actualRanges = new List<(DateTime startDate, DateTime endDate)>(ThetaDataExtensions.GenerateDateRangesWithInterval(startDate, endDate, intervalDays));
39+
40+
Assert.AreEqual(expectedRanges.Count, actualRanges.Count, "The number of ranges should match.");
41+
42+
for (int i = 0; i < expectedRanges.Count; i++)
43+
{
44+
Assert.AreEqual(expectedRanges[i].startDate, actualRanges[i].startDate, $"Start date mismatch at index {i}");
45+
Assert.AreEqual(expectedRanges[i].endDate, actualRanges[i].endDate, $"End date mismatch at index {i}");
46+
}
47+
}
48+
49+
[Test]
50+
public void GenerateDateRangesWithOneDayInterval()
51+
{
52+
var intervalDays = 1;
53+
54+
var startDate = new DateTime(2024, 07, 26);
55+
var endDate = new DateTime(2024, 07, 30);
56+
57+
var expectedRanges = new List<(DateTime startDate, DateTime endDate)>
58+
{
59+
(new DateTime(2024, 07, 26), new DateTime(2024, 07, 27)),
60+
(new DateTime(2024, 07, 28), new DateTime(2024, 07, 29)),
61+
(new DateTime(2024, 07, 30), new DateTime(2024, 07, 30))
62+
};
63+
64+
var actualRanges = new List<(DateTime startDate, DateTime endDate)>(ThetaDataExtensions.GenerateDateRangesWithInterval(startDate, endDate, intervalDays));
65+
66+
Assert.AreEqual(expectedRanges.Count, actualRanges.Count, "The number of ranges should match.");
67+
68+
for (int i = 0; i < expectedRanges.Count; i++)
69+
{
70+
Assert.AreEqual(expectedRanges[i].startDate, actualRanges[i].startDate, $"Start date mismatch at index {i}");
71+
Assert.AreEqual(expectedRanges[i].endDate, actualRanges[i].endDate, $"End date mismatch at index {i}");
72+
}
73+
}
74+
75+
[Test]
76+
public void GenerateDateRangesWithInterval_ShouldHandleSameStartEndDate()
77+
{
78+
DateTime startDate = new DateTime(2025, 2, 1);
79+
DateTime endDate = new DateTime(2025, 2, 1);
80+
81+
var ranges = new List<(DateTime startDate, DateTime endDate)>(
82+
ThetaDataExtensions.GenerateDateRangesWithInterval(startDate, endDate, 1)
83+
);
84+
85+
Assert.AreEqual(1, ranges.Count, "There should be no date ranges generated.");
86+
}
87+
}

QuantConnect.ThetaData.Tests/ThetaDataHistoryProviderTests..cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
using System;
1717
using System.Linq;
1818
using NUnit.Framework;
19+
using QuantConnect.Data;
20+
using System.Diagnostics;
21+
using QuantConnect.Logging;
22+
using Microsoft.CodeAnalysis;
23+
using System.Collections.Generic;
1924

2025
namespace QuantConnect.Lean.DataSource.ThetaData.Tests
2126
{
@@ -114,5 +119,109 @@ public void GetHistoryTickTradeValidateOnDistinctData(string ticker, Resolution
114119

115120
Assert.That(history.Count, Is.EqualTo(distinctHistory.Count));
116121
}
122+
123+
[TestCase("SPY", SecurityType.Equity, Resolution.Hour, "1998/01/02", "2025/02/16", new[] { TickType.Quote, TickType.Trade })]
124+
[TestCase("SPY", SecurityType.Equity, Resolution.Daily, "1998/01/02", "2025/02/16", new[] { TickType.Quote, TickType.Trade })]
125+
[TestCase("SPY", SecurityType.Equity, Resolution.Minute, "2025/01/02", "2025/02/16", new[] { TickType.Quote, TickType.Trade })]
126+
[TestCase("AAPL", SecurityType.Equity, Resolution.Hour, "1998/01/02", "2025/02/16", new[] { TickType.Quote, TickType.Trade })]
127+
public void GetHistoryRequestWithLongRange(string ticker, SecurityType securityType, Resolution resolution, DateTime startDate, DateTime endDate, TickType[] tickTypes)
128+
{
129+
var symbol = TestHelpers.CreateSymbol(ticker, securityType);
130+
131+
var historyRequests = new List<HistoryRequest>();
132+
foreach (var tickType in tickTypes)
133+
{
134+
historyRequests.Add(TestHelpers.CreateHistoryRequest(symbol, resolution, tickType, startDate, endDate));
135+
}
136+
137+
foreach (var historyRequest in historyRequests)
138+
{
139+
var stopwatch = Stopwatch.StartNew();
140+
var history = _thetaDataProvider.GetHistory(historyRequest).ToList();
141+
stopwatch.Stop();
142+
143+
Assert.IsNotEmpty(history);
144+
145+
var firstDate = history.First().Time;
146+
var lastDate = history.Last().Time;
147+
148+
Log.Trace($"[{nameof(ThetaDataHistoryProviderTests)}] Execution completed in {stopwatch.Elapsed.TotalMinutes:F2} min | " +
149+
$"Symbol: {historyRequest.Symbol}, Resolution: {resolution}, TickType: {historyRequest.TickType}, Count: {history.Count}, " +
150+
$"First Date: {firstDate:yyyy-MM-dd HH:mm:ss}, Last Date: {lastDate:yyyy-MM-dd HH:mm:ss}");
151+
152+
// Ensure historical data is returned in chronological order
153+
for (var i = 1; i < history.Count; i++)
154+
{
155+
if (history[i].Time < history[i - 1].Time)
156+
Assert.Fail("Historical data is not in chronological order.");
157+
}
158+
}
159+
}
160+
161+
[TestCase("AAPL", SecurityType.Equity, Resolution.Minute, "2025/02/19", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
162+
[TestCase("AAPL", SecurityType.Equity, Resolution.Minute, "2025/02/18", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
163+
[TestCase("AAPL", SecurityType.Equity, Resolution.Minute, "2025/02/15", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
164+
[TestCase("AAPL", SecurityType.Equity, Resolution.Minute, "2025/02/10", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
165+
[TestCase("AAPL", SecurityType.Equity, Resolution.Hour, "2025/02/19", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
166+
[TestCase("AAPL", SecurityType.Equity, Resolution.Hour, "2025/02/18", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
167+
[TestCase("AAPL", SecurityType.Equity, Resolution.Hour, "2025/02/10", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
168+
[TestCase("AAPL", SecurityType.Equity, Resolution.Hour, "2025/02/01", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
169+
[TestCase("AAPL", SecurityType.Equity, Resolution.Hour, "2025/01/01", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
170+
[TestCase("AAPL", SecurityType.Equity, Resolution.Daily, "2025/02/19", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
171+
[TestCase("AAPL", SecurityType.Equity, Resolution.Daily, "2025/02/18", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
172+
[TestCase("AAPL", SecurityType.Equity, Resolution.Daily, "2025/02/15", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
173+
[TestCase("AAPL", SecurityType.Equity, Resolution.Daily, "2025/02/10", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
174+
[TestCase("AAPL", SecurityType.Equity, Resolution.Daily, "2025/01/01", "2025/02/20", new[] { TickType.Quote, TickType.Trade })]
175+
public void GetHistoryRequestWithCalculateAmountReturnsData(string ticker, SecurityType securityType, Resolution resolution, DateTime startDate, DateTime endDate, TickType[] tickTypes)
176+
{
177+
var symbol = TestHelpers.CreateSymbol(ticker, securityType);
178+
179+
var historyRequests = new List<HistoryRequest>();
180+
foreach (var tickType in tickTypes)
181+
{
182+
historyRequests.Add(TestHelpers.CreateHistoryRequest(symbol, resolution, tickType, startDate, endDate, includeExtendedMarketHours: false));
183+
}
184+
185+
foreach (var historyRequest in historyRequests)
186+
{
187+
var history = _thetaDataProvider.GetHistory(historyRequest).ToList();
188+
//Log.Trace(string.Join("\n", history.Select(x => new { Time = x.Time, EndTime = x.EndTime, Data = x })));
189+
190+
int expectedAmount = CalculateExpectedHistoryAmount(historyRequest);
191+
192+
Assert.AreEqual(expectedAmount, history.Count, "History data count does not match expected amount.");
193+
}
194+
}
195+
196+
private int CalculateExpectedHistoryAmount(HistoryRequest request)
197+
{
198+
var endTime = request.EndTimeUtc.ConvertFromUtc(request.DataTimeZone);
199+
var currentDate = request.StartTimeUtc.ConvertFromUtc(request.DataTimeZone);
200+
int totalDataPoints = 0;
201+
202+
while (currentDate < endTime)
203+
{
204+
if (request.ExchangeHours.IsDateOpen(currentDate, request.IncludeExtendedMarketHours))
205+
{
206+
int dataPointsPerDay = GetDataPointsPerDay(request.Resolution);
207+
totalDataPoints += dataPointsPerDay;
208+
}
209+
210+
currentDate = currentDate.AddDays(1);
211+
}
212+
213+
return totalDataPoints;
214+
}
215+
216+
private int GetDataPointsPerDay(Resolution resolution)
217+
{
218+
return resolution switch
219+
{
220+
Resolution.Minute => 390, // 720 minutes from 9:30 AM to 4:00 PM (Trading Hours)
221+
Resolution.Hour => 7,
222+
Resolution.Daily => 1, // 1 bar per day
223+
_ => throw new ArgumentOutOfRangeException(nameof(resolution), "Unsupported resolution")
224+
};
225+
}
117226
}
118227
}

QuantConnect.ThetaData/Models/Interfaces/ISubscriptionPlan.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,16 @@ namespace QuantConnect.Lean.DataSource.ThetaData.Models.Interfaces;
2020
/// <summary>
2121
/// The <c>ISubscriptionPlan</c> interface defines the base structure for different price plans offered by ThetaData for users.
2222
/// For detailed documentation on ThetaData subscription plans, refer to the following links:
23-
/// <see href="https://www.thetadata.net/subscribe" />
24-
/// <see href="https://http-docs.thetadata.us/docs/theta-data-rest-api-v2/1floxgrco3si8-us-options#historical-endpoint-access" />
23+
/// <list type="bullet">
24+
/// <item>
25+
/// <term>https://www.thetadata.net/subscribe</term>
26+
/// <description>Institutional Data Retail Pricing</description>
27+
/// </item>
28+
/// <item>
29+
/// <term>https://http-docs.thetadata.us/Articles/Getting-Started/Subscriptions.html#options-data</term>
30+
/// <description>Initial Access Date Based on Subscription Plan</description>
31+
/// </item>
32+
///</list>
2533
/// </summary>
2634
public interface ISubscriptionPlan
2735
{
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
namespace QuantConnect.Lean.DataSource.ThetaData.Models.Rest;
17+
18+
/// <summary>
19+
/// Contains constant values for various request parameters used in API queries.
20+
/// </summary>
21+
public static class RequestParameters
22+
{
23+
/// <summary>
24+
/// Represents the time interval in milliseconds since midnight Eastern Time (ET).
25+
/// Example values:
26+
/// - 09:30:00 ET = 34_200_000 ms
27+
/// - 16:00:00 ET = 57_600_000 ms
28+
/// </summary>
29+
public const string IntervalInMilliseconds = "ivl";
30+
31+
/// <summary>
32+
/// Represents the start date for a query or request.
33+
/// </summary>
34+
public const string StartDate = "start_date";
35+
36+
/// <summary>
37+
/// Represents the end date for a query or request.
38+
/// </summary>
39+
public const string EndDate = "end_date";
40+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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.Diagnostics;
17+
using QuantConnect.Logging;
18+
using QuantConnect.Lean.DataSource.ThetaData.Models.Common;
19+
20+
namespace QuantConnect.Lean.DataSource.ThetaData.Models.Wrappers;
21+
22+
/// <summary>
23+
/// A utility class that conditionally starts a stopwatch for measuring execution time
24+
/// when debugging is enabled. Implements <see cref="IDisposable"/> to ensure
25+
/// automatic logging upon completion.
26+
/// </summary>
27+
public class StopwatchWrapper : IDisposable
28+
{
29+
private readonly Stopwatch? _stopwatch;
30+
private readonly string _message;
31+
32+
/// <summary>
33+
/// Initializes a new instance of the <see cref="StopwatchWrapper"/> class
34+
/// and starts a stopwatch to measure execution time.
35+
/// </summary>
36+
/// <param name="message">A descriptive message to include in the log output.</param>
37+
private StopwatchWrapper(string message)
38+
{
39+
_message = message;
40+
_stopwatch = Stopwatch.StartNew();
41+
}
42+
43+
/// <summary>
44+
/// Starts a stopwatch if debugging is enabled and returns an appropriate disposable instance.
45+
/// </summary>
46+
/// <param name="message">A descriptive message to include in the log output.</param>
47+
/// <returns>
48+
/// A <see cref="StopwatchWrapper"/> instance if debugging is enabled,
49+
/// otherwise a no-op <see cref="NullDisposable"/> instance.
50+
/// </returns>
51+
public static IDisposable? StartIfEnabled(string message)
52+
{
53+
return Log.DebuggingEnabled ? new StopwatchWrapper(message) : null;
54+
}
55+
56+
/// <summary>
57+
/// Stops the stopwatch and logs the elapsed time if debugging is enabled.
58+
/// </summary>
59+
public void Dispose()
60+
{
61+
if (_stopwatch != null)
62+
{
63+
_stopwatch.Stop();
64+
Log.Debug($"{_message} completed in {_stopwatch.ElapsedMilliseconds} ms");
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)