diff --git a/Common/Data/InterestRateProvider.cs b/Common/Data/InterestRateProvider.cs index 5c6aad24455a..d55d6decc57d 100644 --- a/Common/Data/InterestRateProvider.cs +++ b/Common/Data/InterestRateProvider.cs @@ -118,7 +118,8 @@ public static Dictionary FromCsvFile(string file, out decimal // skip the first header line, also skip #'s as these are comment lines var interestRateProvider = new Dictionary(); - foreach (var line in dataProvider.ReadLines(file).Skip(1) + var lines = dataProvider != null ? dataProvider.ReadLines(file) : File.ReadLines(file); + foreach (var line in lines.Skip(1) .Where(x => !string.IsNullOrWhiteSpace(x))) { if (TryParse(line, out var date, out var interestRate)) diff --git a/Tests/ToolBox/RandomDataGenerator/OptionUniverseWriterTests.cs b/Tests/ToolBox/RandomDataGenerator/OptionUniverseWriterTests.cs new file mode 100644 index 000000000000..65a3e10e8147 --- /dev/null +++ b/Tests/ToolBox/RandomDataGenerator/OptionUniverseWriterTests.cs @@ -0,0 +1,97 @@ +/* + * 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 System.IO; +using NUnit.Framework; +using QuantConnect.Configuration; +using QuantConnect.Data; +using QuantConnect.Data.Market; +using QuantConnect.Securities; +using QuantConnect.ToolBox.RandomDataGenerator; + +namespace QuantConnect.Tests.ToolBox.RandomDataGenerator; + +/// +/// Unit tests verifying that generates valid daily option universe CSV files. +/// +[TestFixture] +internal class OptionUniverseWriterTests +{ + private string _tempDataFolder = null!; + private string _originalDataFolder = null!; + + [SetUp] + public void SetUp() + { + _tempDataFolder = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(_tempDataFolder); + + _originalDataFolder = Globals.DataFolder; + Config.Set("data-folder", _tempDataFolder); + Globals.Reset(); + } + + [TearDown] + public void TearDown() + { + Config.Set("data-folder", _originalDataFolder); + Globals.Reset(); + + if (Directory.Exists(_tempDataFolder)) + { + Directory.Delete(_tempDataFolder, true); + } + } + + /// + /// Verifies that WriteUniverseFiles aggregates intraday contract ticks and writes the expected daily universe CSV. + /// + [Test] + public void WriteUniverseFiles_GivenOptionTicks_GeneratesMatchingDailyUniverseCsv() + { + var (underlying, optionTickHistories) = CreateSampleOptionTickHistory(); + + OptionUniverseWriter.WriteUniverseFiles(underlying, optionTickHistories); + + var lines = ReadGeneratedUniverseFile("tsla", "20260205"); + Assert.That(lines.Length, Is.EqualTo(3)); + Assert.That(lines[0], Is.EqualTo("expiry,strike,right,open,high,low,close,volume,open_interest,implied_volatility,delta,gamma,vega,theta,rho")); + Assert.That(lines[1], Is.EqualTo(",,,100.0000,100.0000,100.0000,100.0000,0,,,,,,,")); + Assert.That(lines[2], Is.EqualTo("20260213,200,C,1.50,1.75,1.50,1.75,30,20,0.20,,,,,")); + } + + private static (Symbol Underlying, Dictionary> Histories) CreateSampleOptionTickHistory() + { + var underlying = Symbol.Create("TSLA", SecurityType.Equity, Market.USA); + var option = Symbol.CreateOption(underlying, Market.USA, OptionStyle.American, OptionRight.Call, 200m, new DateTime(2026, 2, 13)); + + var ticks = new List + { + new(new DateTime(2026, 2, 5, 9, 30, 0), option, 1.50m, 1.50m) { Quantity = 10 }, + new(new DateTime(2026, 2, 5, 16, 0, 0), option, 1.75m, 1.75m) { Quantity = 20 } + }; + + return (underlying, new Dictionary> { { option, ticks } }); + } + + private string[] ReadGeneratedUniverseFile(string ticker, string date) + { + var path = Path.Combine(_tempDataFolder, "option", "usa", "universes", ticker, $"{date}.csv"); + Assert.That(File.Exists(path), Is.True, $"Universe file was not found at expected path: {path}"); + return File.ReadAllLines(path); + } +} diff --git a/ToolBox/RandomDataGenerator/OptionUniverseWriter.cs b/ToolBox/RandomDataGenerator/OptionUniverseWriter.cs new file mode 100644 index 000000000000..e4db9d3acca3 --- /dev/null +++ b/ToolBox/RandomDataGenerator/OptionUniverseWriter.cs @@ -0,0 +1,149 @@ +/* + * 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 System.IO; +using System.Linq; +using QuantConnect.Data.Market; +using QuantConnect.Data.UniverseSelection; + +namespace QuantConnect.ToolBox.RandomDataGenerator; + +/// +/// Writes daily Option Universe CSV files for generated options data. +/// +internal static class OptionUniverseWriter +{ + private const decimal DefaultSyntheticImpliedVolatility = 0.20m; + private const decimal DefaultUnderlyingPrice = 100.00m; + + private readonly record struct OptionRow( + Symbol Symbol, + decimal Open, + decimal High, + decimal Low, + decimal Close, + decimal Volume, + decimal OpenInterest + ); + + /// + /// Processes generated option tick histories and writes daily universe selection CSVs. + /// + /// The underlying asset symbol. + /// The historical ticks generated for each option contract. + public static void WriteUniverseFiles(Symbol underlyingSymbol, Dictionary> optionTickHistories) + { + var dailyData = AggregateDailyOptionRows(optionTickHistories); + WriteAllDailyUniverseFiles(underlyingSymbol, dailyData); + } + + private static Dictionary> AggregateDailyOptionRows( + Dictionary> optionTickHistories) + { + var dailyContractData = new Dictionary>(); + + foreach (var (contract, ticks) in optionTickHistories) + { + if (ticks.Count == 0) + { + continue; + } + + AggregateContractTicks(contract, ticks, dailyContractData); + } + + return dailyContractData; + } + + private static void AggregateContractTicks( + Symbol contract, + List ticks, + Dictionary> dailyContractData) + { + foreach (var dayGroup in ticks.GroupBy(t => t.Time.Date)) + { + var dayTicks = dayGroup.ToList(); + var row = CreateOptionRow(contract, dayTicks); + + if (!dailyContractData.TryGetValue(dayGroup.Key, out var list)) + { + list = new List(); + dailyContractData[dayGroup.Key] = list; + } + + list.Add(row); + } + } + + private static OptionRow CreateOptionRow(Symbol contract, List dayTicks) + { + var open = dayTicks.First().Value; + var high = dayTicks.Max(t => t.Value); + var low = dayTicks.Min(t => t.Value); + var close = dayTicks.Last().Value; + var volume = dayTicks.Sum(t => t.Quantity); + var openInterest = dayTicks.Last().Quantity; + + return new OptionRow(contract, open, high, low, close, volume, openInterest); + } + + private static void WriteAllDailyUniverseFiles(Symbol underlyingSymbol, Dictionary> dailyData) + { + var canonicalSymbol = Symbol.CreateCanonicalOption(underlyingSymbol); + + foreach (var (date, rows) in dailyData) + { + WriteSingleDailyUniverseFile(canonicalSymbol, date, rows); + } + } + + private static void WriteSingleDailyUniverseFile(Symbol canonicalSymbol, DateTime date, List rows) + { + var universePath = BaseChainUniverseData.GetUniverseFullFilePath(canonicalSymbol, date); + Directory.CreateDirectory(Path.GetDirectoryName(universePath)!); + + var lines = GenerateCsvLines(rows); + File.WriteAllLines(universePath, lines); + } + + private static IEnumerable GenerateCsvLines(List rows) + { + yield return OptionUniverse.CsvHeader(SecurityType.Option); + yield return FormatUnderlyingRow(DefaultUnderlyingPrice); + + var sortedRows = rows.OrderBy(r => r.Symbol.ID.Date).ThenBy(r => r.Symbol.ID.StrikePrice); + foreach (var row in sortedRows) + { + yield return OptionUniverse.ToCsv( + row.Symbol, + row.Open, + row.High, + row.Low, + row.Close, + row.Volume, + row.OpenInterest, + DefaultSyntheticImpliedVolatility, + null + ); + } + } + + private static string FormatUnderlyingRow(decimal price) + { + return FormattableString.Invariant($",,,{price:F4},{price:F4},{price:F4},{price:F4},0,,,,,,,"); + } +} diff --git a/ToolBox/RandomDataGenerator/RandomDataGenerator.cs b/ToolBox/RandomDataGenerator/RandomDataGenerator.cs index da8e2a154d16..11f66731cb03 100644 --- a/ToolBox/RandomDataGenerator/RandomDataGenerator.cs +++ b/ToolBox/RandomDataGenerator/RandomDataGenerator.cs @@ -279,6 +279,15 @@ public void Run() currentCount++; } } + + // Generate daily option universe CSVs if generating options data + if (_settings.SecurityType == SecurityType.Option) + { + var optionTickHistories = tickHistories + .Where(kvp => kvp.Key.SecurityType == SecurityType.Option) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + OptionUniverseWriter.WriteUniverseFiles(symbolRef, optionTickHistories); + } } Log.Trace("RandomDataGenerator.Run(): Random data generation has completed.");