Skip to content

Commit 4f88fae

Browse files
hsm207Martin-Molineroclaude
authored
Generate daily option universe files in RandomDataGenerator (#9740)
* Generate daily option universe files in RandomDataGenerator (#8854) - Automatically generate daily option universe CSV files when generating synthetic options data - Add standalone data provider fallback to InterestRateProvider - Add OptionUniverseWriter unit test suite * Filter option contracts in OptionUniverseWriter to handle mixed security dictionaries * Refactor OptionUniverseWriter to accept filtered option tick histories adhering to SRP * Generate derivative universe files reusing the universe downloader Feed the generator's existing daily aggregators, adding open interest for derivatives, into an in memory IDataDownloader and write the option and future universe files through UniverseExtensions.RunUniverseDownloader, replacing the bespoke option writer. Universe generation is isolated so a failure there logs and does not abort the data generation. Fix the universe file header, which interpolated the CsvHeader method group, and route future rows through FutureUniverse.ToCsv. Revert the unrelated InterestRateProvider change, the data provider is already registered by the ToolBox entry point. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Zic3mz9ZogfCvhzoTXvDG --------- Co-authored-by: Martin Molinero <martin.molinero1@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent fa2eb97 commit 4f88fae

6 files changed

Lines changed: 288 additions & 4 deletions

File tree

Common/Data/UniverseSelection/DerivativeUniverseData.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ public void UpdateByOpenInterest(OpenInterest openInterest)
117117
/// <returns>A CSV formatted string representing the data.</returns>
118118
public string ToCsv()
119119
{
120+
if (_symbol.SecurityType == SecurityType.Future)
121+
{
122+
return FutureUniverse.ToCsv(_symbol, _open, _high, _low, _close, _volume, _openInterest);
123+
}
120124
return OptionUniverse.ToCsv(_symbol, _open, _high, _low, _close, _volume, _openInterest, null, NullGreeks.Instance);
121125
}
122126
}

Common/Data/UniverseSelection/UniverseExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ public static void RunUniverseDownloader(IDataDownloader dataDownloader, DataUni
206206

207207
using var writer = new StreamWriter(universeDownloadParameters.GetUniverseFileName(processingDate));
208208

209-
writer.WriteLine($"#{OptionUniverse.CsvHeader}");
209+
var securityType = universeDownloadParameters.Symbol.SecurityType;
210+
writer.WriteLine($"#{(securityType == SecurityType.Future ? FutureUniverse.CsvHeader : OptionUniverse.CsvHeader(securityType))}");
210211

211212
// Write option data, sorted by contract type (Call/Put), strike price, expiration date, and then by full ID
212213
foreach (var universeData in universeDataBySymbol
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 QuantConnect.Data.Market;
19+
using QuantConnect.Data.UniverseSelection;
20+
21+
namespace QuantConnect.Tests.Common.Data.UniverseSelection
22+
{
23+
[TestFixture]
24+
public class DerivativeUniverseDataTests
25+
{
26+
[Test]
27+
public void FutureRowUsesTheContractMonth()
28+
{
29+
var symbol = Symbol.CreateFuture("ES", QuantConnect.Market.CME, new DateTime(2020, 3, 20));
30+
var data = new DerivativeUniverseData(new TradeBar(new DateTime(2020, 1, 6), symbol, 10, 12, 9, 11, 100));
31+
data.UpdateByOpenInterest(new OpenInterest(new DateTime(2020, 1, 6), symbol, 5000));
32+
33+
Assert.AreEqual("202003,10,12,9,11,100,5000", data.ToCsv());
34+
}
35+
36+
[Test]
37+
public void OptionRowUsesTheContractDetails()
38+
{
39+
var symbol = Symbol.CreateOption(Symbols.SPY, QuantConnect.Market.USA, OptionStyle.American, OptionRight.Put, 300, new DateTime(2020, 3, 20));
40+
var data = new DerivativeUniverseData(new TradeBar(new DateTime(2020, 1, 6), symbol, 10, 12, 9, 11, 100));
41+
data.UpdateByOpenInterest(new OpenInterest(new DateTime(2020, 1, 6), symbol, 5000));
42+
43+
Assert.AreEqual("20200320,300,P,10,12,9,11,100,5000,,0,0,0,0,0", data.ToCsv());
44+
}
45+
46+
[Test]
47+
public void UnderlyingRowHasNoContractDetails()
48+
{
49+
var data = new DerivativeUniverseData(new TradeBar(new DateTime(2020, 1, 6), Symbols.SPY, 10, 12, 9, 11, 100));
50+
51+
Assert.AreEqual(",,,10,12,9,11,100,,,0,0,0,0,0", data.ToCsv());
52+
}
53+
}
54+
}

Tests/ToolBox/RandomDataGenerator/RandomDataGeneratorTests.cs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using QuantConnect.Data;
2121
using QuantConnect.ToolBox.RandomDataGenerator;
2222
using QuantConnect.Data.Market;
23+
using QuantConnect.Data.UniverseSelection;
2324
using QuantConnect.Lean.Engine.DataFeeds.Enumerators;
2425
using QuantConnect.Configuration;
2526
using QuantConnect.Data.Auxiliary;
@@ -222,6 +223,116 @@ public void RandomDataGeneratorCompletesSuccessfully()
222223
}
223224
}
224225

226+
[TestCase(SecurityType.Option, Market.USA, "AAPL", Resolution.Minute)]
227+
[TestCase(SecurityType.Future, Market.CME, "ES", Resolution.Minute)]
228+
[TestCase(SecurityType.Future, Market.CME, "ES", Resolution.Hour)]
229+
[TestCase(SecurityType.Future, Market.CME, "ES", Resolution.Daily)]
230+
[TestCase(SecurityType.Future, Market.CME, "ES", Resolution.Tick)]
231+
public void RandomDataGeneratorWritesDerivativeUniverseFiles(SecurityType securityType, string market, string ticker, Resolution resolution)
232+
{
233+
var tempFolder = Path.Combine(Path.GetTempPath(), $"LeanTest_{Guid.NewGuid()}");
234+
var originalDataFolder = Config.Get("data-folder");
235+
try
236+
{
237+
Directory.CreateDirectory(tempFolder);
238+
Config.Set("data-folder", tempFolder);
239+
Globals.Reset();
240+
241+
var settings = new RandomDataGeneratorSettings
242+
{
243+
Start = new DateTime(2020, 1, 6),
244+
End = new DateTime(2020, 1, 10),
245+
SymbolCount = 1,
246+
Market = market,
247+
SecurityType = securityType,
248+
Resolution = resolution,
249+
// keep the minute option case fast, the option price model is expensive
250+
DataDensity = resolution == Resolution.Minute ? DataDensity.Sparse : DataDensity.Dense,
251+
IncludeCoarse = false,
252+
QuoteTradeRatio = 1.0,
253+
RandomSeed = 123456,
254+
RandomSeedSet = true,
255+
ChainSymbolCount = 2,
256+
OptionPriceEngineName = "BaroneAdesiWhaleyApproximationEngine",
257+
Tickers = new List<string>() { ticker }
258+
};
259+
260+
var generator = GetGenerator(settings);
261+
Assert.DoesNotThrow(() => generator.Run());
262+
263+
var canonical = securityType == SecurityType.Future
264+
? Symbol.Create(ticker, SecurityType.Future, market)
265+
: Symbol.CreateCanonicalOption(Symbol.Create(ticker, SecurityType.Equity, market));
266+
var universeFiles = Directory.GetFiles(LeanData.GenerateUniversesDirectory(tempFolder, canonical), "*.csv");
267+
Assert.IsNotEmpty(universeFiles);
268+
269+
var config = new SubscriptionDataConfig(securityType == SecurityType.Future ? typeof(FutureUniverse) : typeof(OptionUniverse),
270+
canonical, Resolution.Daily, TimeZones.NewYork, TimeZones.NewYork, true, true, false);
271+
BaseChainUniverseData factory = securityType == SecurityType.Future ? new FutureUniverse() : new OptionUniverse();
272+
var expectedHeader = securityType == SecurityType.Future ? FutureUniverse.CsvHeader : OptionUniverse.CsvHeader(securityType);
273+
var expectedContractsCount = securityType == SecurityType.Future ? 1 : settings.ChainSymbolCount * 2;
274+
var maxContractsCount = 0;
275+
var anyOpenInterest = false;
276+
var anyContractPrice = false;
277+
foreach (var universeFile in universeFiles)
278+
{
279+
var date = DateTime.ParseExact(Path.GetFileNameWithoutExtension(universeFile), DateFormat.EightCharacter, null);
280+
Assert.IsTrue(date >= settings.Start && date <= settings.End, universeFile);
281+
282+
var lines = File.ReadAllLines(universeFile);
283+
Assert.AreEqual($"#{expectedHeader}", lines[0], universeFile);
284+
285+
// make sure Lean can read them back
286+
var rows = new List<BaseChainUniverseData>();
287+
using var reader = new StreamReader(universeFile);
288+
while (!reader.EndOfStream)
289+
{
290+
var data = (BaseChainUniverseData)factory.Reader(config, reader, date, false);
291+
if (data != null)
292+
{
293+
rows.Add(data);
294+
}
295+
}
296+
Assert.AreEqual(lines.Length - 1, rows.Count, universeFile);
297+
298+
// options have an underlying data row first, then the contracts
299+
var contracts = rows.Where(x => x.Symbol.SecurityType == securityType).ToList();
300+
Assert.AreEqual(securityType == SecurityType.Future ? 0 : 1, rows.Count - contracts.Count, universeFile);
301+
Assert.IsTrue(securityType == SecurityType.Future || rows[0].Symbol == canonical.Underlying, universeFile);
302+
// options warm up on the first underlying data points, so the first days might have no contracts
303+
Assert.LessOrEqual(contracts.Count, expectedContractsCount, universeFile);
304+
maxContractsCount = Math.Max(maxContractsCount, contracts.Count);
305+
306+
foreach (var row in rows)
307+
{
308+
Assert.IsFalse(row.Symbol.IsCanonical(), universeFile);
309+
Assert.AreEqual(canonical.ID.Symbol, row.Symbol.ID.Symbol, universeFile);
310+
if (row.Symbol.SecurityType == securityType)
311+
{
312+
Assert.GreaterOrEqual(row.Symbol.ID.Date, date, universeFile);
313+
// the price model can price a contract at zero and open interest is generated once a day, starting the second day
314+
anyContractPrice |= row.Close > 0 && row.Volume > 0;
315+
anyOpenInterest |= row.OpenInterest > 0;
316+
}
317+
else
318+
{
319+
Assert.Greater(row.Close, 0, universeFile);
320+
Assert.Greater(row.Volume, 0, universeFile);
321+
}
322+
}
323+
}
324+
Assert.AreEqual(expectedContractsCount, maxContractsCount);
325+
Assert.IsTrue(anyContractPrice);
326+
Assert.IsTrue(anyOpenInterest);
327+
}
328+
finally
329+
{
330+
Config.Set("data-folder", originalDataFolder);
331+
Globals.Reset();
332+
Directory.Delete(tempFolder, true);
333+
}
334+
}
335+
225336
private static QuantConnect.ToolBox.RandomDataGenerator.RandomDataGenerator GetGenerator(RandomDataGeneratorSettings settings)
226337
{
227338
var securityManager = new SecurityManager(new TimeKeeper(settings.Start, new[] { TimeZones.Utc }));
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.Collections.Generic;
17+
using System.Linq;
18+
using QuantConnect.Data;
19+
using QuantConnect.Securities;
20+
21+
namespace QuantConnect.ToolBox.RandomDataGenerator
22+
{
23+
/// <summary>
24+
/// <see cref="IDataDownloader"/> implementation serving the generated data it was fed,
25+
/// used to create the derivative universe files with the same code path as the data downloaders
26+
/// </summary>
27+
public class InMemoryDataDownloader : IDataDownloader
28+
{
29+
private readonly List<(TickType TickType, BaseData Data)> _data = new();
30+
31+
/// <summary>
32+
/// The exchange hours of the data this instance holds
33+
/// </summary>
34+
public SecurityExchangeHours ExchangeHours { get; }
35+
36+
/// <summary>
37+
/// Creates a new instance
38+
/// </summary>
39+
/// <param name="exchangeHours">The exchange hours of the data this instance will hold, which is in the exchange time zone</param>
40+
public InMemoryDataDownloader(SecurityExchangeHours exchangeHours)
41+
{
42+
ExchangeHours = exchangeHours;
43+
}
44+
45+
/// <summary>
46+
/// Adds data to serve
47+
/// </summary>
48+
/// <param name="tickType">The tick type of the data</param>
49+
/// <param name="data">The data to add, in the exchange time zone</param>
50+
public void Add(TickType tickType, IEnumerable<BaseData> data)
51+
{
52+
_data.AddRange(data.Select(x => (tickType, x)));
53+
}
54+
55+
/// <summary>
56+
/// Get historical data enumerable for a single symbol, type and resolution given this start and end time (in UTC).
57+
/// Requests for a canonical symbol will return the data of all its contracts
58+
/// </summary>
59+
/// <param name="dataDownloaderGetParameters">model class for passing in parameters for historical data</param>
60+
/// <returns>Enumerable of base data for this symbol</returns>
61+
public IEnumerable<BaseData> Get(DataDownloaderGetParameters dataDownloaderGetParameters)
62+
{
63+
var symbol = dataDownloaderGetParameters.Symbol;
64+
var start = dataDownloaderGetParameters.StartUtc.ConvertFromUtc(ExchangeHours.TimeZone);
65+
var end = dataDownloaderGetParameters.EndUtc.ConvertFromUtc(ExchangeHours.TimeZone);
66+
67+
return _data
68+
.Where(x => x.TickType == dataDownloaderGetParameters.TickType
69+
&& x.Data.Time >= start && x.Data.Time < end
70+
&& (symbol.IsCanonical() ? x.Data.Symbol.HasCanonical() && x.Data.Symbol.Canonical == symbol : x.Data.Symbol == symbol))
71+
.Select(x => x.Data);
72+
}
73+
}
74+
}

ToolBox/RandomDataGenerator/RandomDataGenerator.cs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
using QuantConnect.Data;
1717
using QuantConnect.Data.Auxiliary;
1818
using QuantConnect.Data.Market;
19+
using QuantConnect.Data.UniverseSelection;
1920
using QuantConnect.Securities;
2021
using System;
2122
using System.Collections.Generic;
23+
using System.IO;
2224
using System.Linq;
2325
using QuantConnect.Lean.Engine.DataFeeds.Enumerators;
2426
using QuantConnect.Logging;
27+
using QuantConnect.Util;
2528

2629
namespace QuantConnect.ToolBox.RandomDataGenerator
2730
{
@@ -74,6 +77,8 @@ public void Run()
7477
var count = 0;
7578
var progress = 0d;
7679
var previousMonth = -1;
80+
// daily data of the generated derivative contracts and their underlying, by canonical symbol, used to generate the universe files
81+
var universeDownloaders = new Dictionary<Symbol, InMemoryDataDownloader>();
7782

7883
foreach (var (symbolRef, currentSymbolGroup) in symbolGenerator.GenerateRandomSymbols()
7984
.GroupBy(s => s.HasUnderlying ? s.Underlying : s)
@@ -84,6 +89,7 @@ public void Run()
8489
var tickGenerators = new List<IEnumerator<Tick>>();
8590
var tickHistories = new Dictionary<Symbol, List<Tick>>();
8691
Security underlyingSecurity = null;
92+
InMemoryDataDownloader universeDownloader = null;
8793
foreach (var currentSymbol in currentSymbolGroup)
8894
{
8995
if (!_securityManager.TryGetValue(currentSymbol, out var security))
@@ -97,6 +103,11 @@ public void Run()
97103

98104
underlyingSecurity ??= security;
99105

106+
if (currentSymbol.HasCanonical() && !universeDownloaders.TryGetValue(currentSymbol.Canonical, out universeDownloader))
107+
{
108+
universeDownloader = universeDownloaders[currentSymbol.Canonical] = new InMemoryDataDownloader(security.Exchange.Hours);
109+
}
110+
100111
tickGenerators.Add(
101112
new TickGenerator(_settings, tickTypesPerSecurityType[currentSymbol.SecurityType].ToArray(), security, randomValueGenerator)
102113
.GenerateTicks()
@@ -268,7 +279,14 @@ public void Run()
268279
// lest we likely wouldn't get the last piece of data stuck in the consolidator
269280
// Filter out the data we're going to write here because filtering them in the consolidator update phase
270281
// makes it write all dates for some unknown reason
271-
writer.Write(item.Flush().Where(data => data.Time > previousRenameDate && previousRenameDateDay != DataDay(data)));
282+
var consolidated = item.Flush().Where(data => data.Time > previousRenameDate && previousRenameDateDay != DataDay(data)).ToList();
283+
writer.Write(consolidated);
284+
285+
if (item.Resolution == Resolution.Daily)
286+
{
287+
// the daily data is what we use to generate the derivative universe files
288+
universeDownloader?.Add(item.TickType, consolidated);
289+
}
272290
}
273291

274292
// update progress
@@ -281,6 +299,22 @@ public void Run()
281299
}
282300
}
283301

302+
foreach (var (canonicalSymbol, universeDownloader) in universeDownloaders)
303+
{
304+
Log.Trace($"RandomDataGenerator.Run(): {canonicalSymbol} - Generating universe files...");
305+
try
306+
{
307+
Directory.CreateDirectory(LeanData.GenerateUniversesDirectory(Globals.DataFolder, canonicalSymbol));
308+
UniverseExtensions.RunUniverseDownloader(universeDownloader,
309+
new DataUniverseDownloaderGetParameters(canonicalSymbol, _settings.Start, _settings.End, universeDownloader.ExchangeHours));
310+
}
311+
catch (Exception exception)
312+
{
313+
// the universe files are a nice to have, let's not fail the whole generation
314+
Log.Error(exception, $"RandomDataGenerator.Run(): {canonicalSymbol} - Failed to generate universe files");
315+
}
316+
}
317+
284318
Log.Trace("RandomDataGenerator.Run(): Random data generation has completed.");
285319

286320
DateTime TickDay(Tick tick) => new(tick.Time.Year, tick.Time.Month, tick.Time.Day);
@@ -316,8 +350,9 @@ public static IEnumerable<TickAggregator> CreateAggregators(RandomDataGeneratorS
316350
}
317351

318352

319-
// ensure we have a daily consolidator when coarse is enabled
320-
if (settings.IncludeCoarse && settings.Resolution != Resolution.Daily)
353+
// ensure we have a daily consolidator when coarse is enabled or for derivatives, whose universe files are generated from daily data
354+
var isDerivative = settings.SecurityType.IsOption() || settings.SecurityType == SecurityType.Future;
355+
if ((settings.IncludeCoarse || isDerivative) && settings.Resolution != Resolution.Daily)
321356
{
322357
// prefer trades for coarse - in practice equity only does trades, but leaving this as configurable
323358
if (tickTypes.Contains(TickType.Trade))
@@ -328,6 +363,11 @@ public static IEnumerable<TickAggregator> CreateAggregators(RandomDataGeneratorS
328363
{
329364
yield return TickAggregator.ForTickTypes(settings.SecurityType, Resolution.Daily, TickType.Quote).Single();
330365
}
366+
367+
if (isDerivative && tickTypes.Contains(TickType.OpenInterest))
368+
{
369+
yield return TickAggregator.ForTickTypes(settings.SecurityType, Resolution.Daily, TickType.OpenInterest).Single();
370+
}
331371
}
332372
}
333373
}

0 commit comments

Comments
 (0)