Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Common/Data/InterestRateProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ public static Dictionary<DateTime, decimal> FromCsvFile(string file, out decimal

// skip the first header line, also skip #'s as these are comment lines
var interestRateProvider = new Dictionary<DateTime, decimal>();
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))
Expand Down
97 changes: 97 additions & 0 deletions Tests/ToolBox/RandomDataGenerator/OptionUniverseWriterTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Unit tests verifying that <see cref="OptionUniverseWriter"/> generates valid daily option universe CSV files.
/// </summary>
[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);
}
}

/// <summary>
/// Verifies that WriteUniverseFiles aggregates intraday contract ticks and writes the expected daily universe CSV.
/// </summary>
[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<Symbol, List<Tick>> 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<Tick>
{
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<Symbol, List<Tick>> { { 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);
}
}
149 changes: 149 additions & 0 deletions ToolBox/RandomDataGenerator/OptionUniverseWriter.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Writes daily Option Universe CSV files for generated options data.
/// </summary>
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
);

/// <summary>
/// Processes generated option tick histories and writes daily universe selection CSVs.
/// </summary>
/// <param name="underlyingSymbol">The underlying asset symbol.</param>
/// <param name="optionTickHistories">The historical ticks generated for each option contract.</param>
public static void WriteUniverseFiles(Symbol underlyingSymbol, Dictionary<Symbol, List<Tick>> optionTickHistories)
{
var dailyData = AggregateDailyOptionRows(optionTickHistories);
WriteAllDailyUniverseFiles(underlyingSymbol, dailyData);
}

private static Dictionary<DateTime, List<OptionRow>> AggregateDailyOptionRows(
Dictionary<Symbol, List<Tick>> optionTickHistories)
{
var dailyContractData = new Dictionary<DateTime, List<OptionRow>>();

foreach (var (contract, ticks) in optionTickHistories)
{
if (ticks.Count == 0)
{
continue;
}

AggregateContractTicks(contract, ticks, dailyContractData);
}

return dailyContractData;
}

private static void AggregateContractTicks(
Symbol contract,
List<Tick> ticks,
Dictionary<DateTime, List<OptionRow>> 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<OptionRow>();
dailyContractData[dayGroup.Key] = list;
}

list.Add(row);
}
}

private static OptionRow CreateOptionRow(Symbol contract, List<Tick> 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<DateTime, List<OptionRow>> 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<OptionRow> rows)
{
var universePath = BaseChainUniverseData.GetUniverseFullFilePath(canonicalSymbol, date);
Directory.CreateDirectory(Path.GetDirectoryName(universePath)!);

var lines = GenerateCsvLines(rows);
File.WriteAllLines(universePath, lines);
}

private static IEnumerable<string> GenerateCsvLines(List<OptionRow> 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,,,,,,,");
}
}
9 changes: 9 additions & 0 deletions ToolBox/RandomDataGenerator/RandomDataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down