chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:02:50 +08:00
commit 0fc60fdcb1
5008 changed files with 910633 additions and 0 deletions
@@ -0,0 +1,253 @@
/*
* 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 QuantConnect.Securities;
using QuantConnect.Util;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Provide the base symbol generator implementation
/// </summary>
public abstract class BaseSymbolGenerator
{
/// <summary>
/// <see cref="IRandomValueGenerator"/> instance producing random values for use in random data generation
/// </summary>
protected IRandomValueGenerator Random { get; }
/// <summary>
/// Settings of current random data generation run
/// </summary>
protected RandomDataGeneratorSettings Settings { get; }
/// <summary>
/// Exchange hours and raw data times zones in various markets
/// </summary>
protected MarketHoursDatabase MarketHoursDatabase { get; }
/// <summary>
/// Access to specific properties for various symbols
/// </summary>
protected SymbolPropertiesDatabase SymbolPropertiesDatabase { get; }
// used to prevent generating duplicates, but also caps
// the memory allocated to checking for duplicates
private readonly FixedSizeHashQueue<Symbol> _symbols;
/// <summary>
/// Base constructor implementation for Symbol generator
/// </summary>
/// <param name="settings">random data generation run settings</param>
/// <param name="random">produces random values for use in random data generation</param>
protected BaseSymbolGenerator(RandomDataGeneratorSettings settings, IRandomValueGenerator random)
{
Settings = settings;
Random = random;
_symbols = new FixedSizeHashQueue<Symbol>(1000);
SymbolPropertiesDatabase = SymbolPropertiesDatabase.FromDataFolder();
MarketHoursDatabase = MarketHoursDatabase.FromDataFolder();
}
/// <summary>
/// Creates a ad-hoc symbol generator depending on settings
/// </summary>
/// <param name="settings">random data generator settings</param>
/// <param name="random">produces random values for use in random data generation</param>
/// <returns>New symbol generator</returns>
public static BaseSymbolGenerator Create(RandomDataGeneratorSettings settings, IRandomValueGenerator random)
{
if (settings is null)
{
throw new ArgumentNullException(nameof(settings), "Settings cannot be null or empty");
}
if (random is null)
{
throw new ArgumentNullException(nameof(random), "Randomizer cannot be null");
}
switch (settings.SecurityType)
{
case SecurityType.Option:
return new OptionSymbolGenerator(settings, random, 100m, 75m);
case SecurityType.Future:
return new FutureSymbolGenerator(settings, random);
default:
return new DefaultSymbolGenerator(settings, random);
}
}
/// <summary>
/// Generates specified number of symbols
/// </summary>
/// <returns>Set of random symbols</returns>
public IEnumerable<Symbol> GenerateRandomSymbols()
{
if (!Settings.Tickers.IsNullOrEmpty())
{
foreach (var symbol in Settings.Tickers.SelectMany(GenerateAsset))
{
yield return symbol;
}
}
else
{
for (var i = 0; i < Settings.SymbolCount; i++)
{
foreach (var symbol in GenerateAsset())
{
yield return symbol;
}
}
}
}
/// <summary>
/// Generates a random asset
/// </summary>
/// <param name="ticker">Optionally can provide a ticker that should be used</param>
/// <returns>Random asset</returns>
protected abstract IEnumerable<Symbol> GenerateAsset(string ticker = null);
/// <summary>
/// Generates random symbol, used further down for asset
/// </summary>
/// <param name="securityType">security type</param>
/// <param name="market">market</param>
/// <param name="ticker">Optionally can provide a ticker to use</param>
/// <returns>Random symbol</returns>
public Symbol NextSymbol(SecurityType securityType, string market, string ticker = null)
{
if (securityType == SecurityType.Option || securityType == SecurityType.Future)
{
throw new ArgumentException("Please use OptionSymbolGenerator or FutureSymbolGenerator for SecurityType.Option and SecurityType.Future respectively.");
}
if (ticker == null)
{
// we must return a Symbol matching an entry in the Symbol properties database
// if there is a wildcard entry, we can generate a truly random Symbol
// if there is no wildcard entry, the symbols we can generate are limited by the entries in the database
if (SymbolPropertiesDatabase.ContainsKey(market, SecurityDatabaseKey.Wildcard, securityType))
{
// let's make symbols all have 3 chars as it's acceptable for all security types with wildcard entries
ticker = NextUpperCaseString(3, 3);
}
else
{
ticker = NextTickerFromSymbolPropertiesDatabase(securityType, market);
}
}
// by chance we may generate a ticker that actually exists, and if map files exist that match this
// ticker then we'll end up resolving the first trading date for use in the SID, otherwise, all
// generated Symbol will have a date equal to SecurityIdentifier.DefaultDate
var symbol = Symbol.Create(ticker, securityType, market);
if (_symbols.Add(symbol))
{
return symbol;
}
// lo' and behold, we created a duplicate --recurse to find a unique value
// this is purposefully done as the last statement to enable the compiler to
// unroll this method into a tail-recursion loop :)
return NextSymbol(securityType, market);
}
/// <summary>
/// Return a Ticker matching an entry in the Symbol properties database
/// </summary>
/// <param name="securityType">security type</param>
/// <param name="market"></param>
/// <returns>Random Ticker matching an entry in the Symbol properties database</returns>
protected string NextTickerFromSymbolPropertiesDatabase(SecurityType securityType, string market)
{
// prevent returning a ticker matching any previously generated Symbol
var existingTickers = _symbols
.Where(sym => sym.ID.Market == market && sym.ID.SecurityType == securityType)
.Select(sym => sym.Value);
// get the available tickers from the Symbol properties database and remove previously generated tickers
var availableTickers = Enumerable.Except(SymbolPropertiesDatabase.GetSymbolPropertiesList(market, securityType)
.Select(kvp => kvp.Key.Symbol), existingTickers)
.ToList();
// there is a limited number of entries in the Symbol properties database so we may run out of tickers
if (availableTickers.Count == 0)
{
throw new NoTickersAvailableException(securityType, market);
}
return availableTickers[Random.NextInt(availableTickers.Count)];
}
/// <summary>
/// Generates random expiration date on a friday within specified time range
/// </summary>
/// <param name="marketHours">market hours</param>
/// <param name="minExpiry">minimum expiration date</param>
/// <param name="maxExpiry">maximum expiration date</param>
/// <returns>Random date on a friday within specified time range</returns>
protected DateTime GetRandomExpiration(SecurityExchangeHours marketHours, DateTime minExpiry, DateTime maxExpiry)
{
// generate a random expiration date on a friday
var expiry = Random.NextDate(minExpiry, maxExpiry, DayOfWeek.Friday);
// check to see if we're open on this date and if not, back track until we are
// we're using the equity market hours as a proxy since we haven't generated the option Symbol yet
while (!marketHours.IsDateOpen(expiry))
{
expiry = expiry.AddDays(-1);
}
return expiry;
}
/// <summary>
/// Generates a random <see cref="string"/> within the specified lengths.
/// </summary>
/// <param name="minLength">The minimum length, inclusive</param>
/// <param name="maxLength">The maximum length, inclusive</param>
/// <returns>A new upper case string within the specified lengths</returns>
public string NextUpperCaseString(int minLength, int maxLength)
{
var str = string.Empty;
var length = Random.NextInt(minLength, maxLength);
for (int i = 0; i < length; i++)
{
// A=65 - inclusive lower bound
// Z=90 - inclusive upper bound
var c = (char)Random.NextInt(65, 91);
str += c;
}
return str;
}
/// <summary>
/// Returns the number of symbols with the specified parameters can be generated.
/// Returns int.MaxValue if there is no limit for the given parameters.
/// </summary>
/// <returns>The number of available symbols for the given parameters, or int.MaxValue if no limit</returns>
public abstract int GetAvailableSymbolCount();
}
}
@@ -0,0 +1,23 @@
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Specifies how dense data should be generated
/// </summary>
public enum DataDensity
{
/// <summary>
/// At least once per resolution step
/// </summary>
Dense,
/// <summary>
/// At least once per 5 resolution steps
/// </summary>
Sparse,
/// <summary>
/// At least once per 50 resolution steps
/// </summary>
VerySparse
}
}
@@ -0,0 +1,72 @@
/*
* 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.Linq;
using QuantConnect.Securities;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Generates a new random <see cref="Symbol"/> object of the specified security type.
/// All returned symbols have a matching entry in the Symbol properties database.
/// </summary>
/// <remarks>
/// A valid implementation will keep track of generated Symbol objects to ensure duplicates
/// are not generated.
/// </remarks>
public class DefaultSymbolGenerator : BaseSymbolGenerator
{
private readonly string _market;
private readonly SecurityType _securityType;
/// <summary>
/// Creates <see cref="DefaultSymbolGenerator"/> instance
/// </summary>
/// <param name="settings">random data generation run settings</param>
/// <param name="random">produces random values for use in random data generation</param>
public DefaultSymbolGenerator(RandomDataGeneratorSettings settings, IRandomValueGenerator random)
: base(settings, random)
{
_market = settings.Market;
_securityType = settings.SecurityType;
}
/// <summary>
/// Generates a single-item list at a time using base random implementation
/// </summary>
/// <returns></returns>
protected override IEnumerable<Symbol> GenerateAsset(string ticker = null)
{
yield return NextSymbol(Settings.SecurityType, Settings.Market, ticker);
}
/// <summary>
/// Returns the number of symbols with the specified parameters can be generated.
/// Returns int.MaxValue if there is no limit for the given parameters.
/// </summary>
/// <returns>The number of available symbols for the given parameters, or int.MaxValue if no limit</returns>
public override int GetAvailableSymbolCount()
{
// check the Symbol properties database to determine how many symbols we can generate
// if there is a wildcard entry, we can generate as many symbols as we want
// if there is no wildcard entry, we can only generate as many symbols as there are entries
return SymbolPropertiesDatabase.ContainsKey(_market, SecurityDatabaseKey.Wildcard, _securityType)
? int.MaxValue
: SymbolPropertiesDatabase.GetSymbolPropertiesList(_market, _securityType).Count();
}
}
}
@@ -0,0 +1,265 @@
/*
* 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.Linq;
using QuantConnect.Data.Market;
using QuantConnect.Data.Auxiliary;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Generates random splits, random dividends, and map file
/// </summary>
public class DividendSplitMapGenerator
{
private const double _minimumFinalSplitFactorAllowed = 0.001;
/// <summary>
/// The final factor to adjust all prices with in order to maintain price continuity.
/// </summary>
/// <remarks>
/// Set default equal to 1 so that we can use it even in the event of no splits
/// </remarks>
public decimal FinalSplitFactor { get; set; } = 1m;
/// <summary>
/// Stores <see cref="MapFileRow"/> instances
/// </summary>
public List<MapFileRow> MapRows { get; set; } = new();
/// <summary>
/// Stores <see cref="CorporateFactorRow"/> instances
/// </summary>
public List<CorporateFactorRow> DividendsSplits { get; set; } = new List<CorporateFactorRow>();
/// <summary>
/// Current Symbol value. Can be renamed
/// </summary>
public Symbol CurrentSymbol { get; private set; }
private readonly RandomValueGenerator _randomValueGenerator;
private readonly Random _random;
private readonly RandomDataGeneratorSettings _settings;
private readonly DateTime _delistDate;
private readonly bool _willBeDelisted;
private readonly BaseSymbolGenerator _symbolGenerator;
public DividendSplitMapGenerator(
Symbol symbol,
RandomDataGeneratorSettings settings,
RandomValueGenerator randomValueGenerator,
BaseSymbolGenerator symbolGenerator,
Random random,
DateTime delistDate,
bool willBeDelisted)
{
CurrentSymbol = symbol;
_settings = settings;
_randomValueGenerator = randomValueGenerator;
_random = random;
_delistDate = delistDate;
_willBeDelisted = willBeDelisted;
_symbolGenerator = symbolGenerator;
}
/// <summary>
/// Generates the splits, dividends, and maps.
/// Writes necessary output to public variables
/// </summary>
/// <param name="tickHistory"></param>
public void GenerateSplitsDividends(IEnumerable<Tick> tickHistory)
{
var previousMonth = -1;
var monthsTrading = 0;
var hasRename = _randomValueGenerator.NextBool(_settings.HasRenamePercentage);
var hasSplits = _randomValueGenerator.NextBool(_settings.HasSplitsPercentage);
var hasDividends = _randomValueGenerator.NextBool(_settings.HasDividendsPercentage);
var dividendEveryQuarter = _randomValueGenerator.NextBool(_settings.DividendEveryQuarterPercentage);
var previousX = _random.NextDouble();
// Since the largest equity value we can obtain is 1 000 000, if we want this price divided by the FinalSplitFactor
// to be upper bounded by 1 000 000 000 we need to make sure the FinalSplitFactor is lower bounded by 0.001. Therefore,
// since in the worst of the cases FinalSplitFactor = (previousSplitFactor)^(2m), where m is the number of months
// in the time span, we need to lower bound previousSplitFactor by (0.001)^(1/(2m))
//
// On the other hand, if the upper bound for the previousSplitFactor is 1, then the FinalSplitFactor will be, in the
// worst of the cases as small as the minimum equity value we can obtain
var months = (int)Math.Round(_settings.End.Subtract(_settings.Start).Days / (365.25 / 12));
months = months != 0 ? months : 1;
var minPreviousSplitFactor = GetLowerBoundForPreviousSplitFactor(months);
var maxPreviousSplitFactor = 1;
var previousSplitFactor = hasSplits ? GetNextPreviousSplitFactor(_random, minPreviousSplitFactor, maxPreviousSplitFactor) : 1;
var previousPriceFactor = hasDividends ? (decimal)Math.Tanh(previousX) : 1;
var splitDates = new List<DateTime>();
var dividendDates = new List<DateTime>();
var firstTick = true;
// Iterate through all ticks and generate splits and dividend data
if (_settings.SecurityType == SecurityType.Equity)
{
foreach (var tick in tickHistory)
{
// On the first trading day write relevant starting data to factor and map files
if (firstTick)
{
DividendsSplits.Add(new CorporateFactorRow(tick.Time,
previousPriceFactor,
previousSplitFactor,
tick.Value));
MapRows.Add(new MapFileRow(tick.Time, CurrentSymbol.Value));
}
// Add the split to the DividendsSplits list if we have a pending
// split. That way, we can use the correct referencePrice in the split event.
if (splitDates.Count != 0)
{
var deleteDates = new List<DateTime>();
foreach (var splitDate in splitDates)
{
if (tick.Time > splitDate)
{
DividendsSplits.Add(new CorporateFactorRow(
splitDate,
previousPriceFactor,
previousSplitFactor,
tick.Value / FinalSplitFactor));
FinalSplitFactor *= previousSplitFactor;
deleteDates.Add(splitDate);
}
}
// Deletes dates we've already looped over
splitDates.RemoveAll(x => deleteDates.Contains(x));
}
if (dividendDates.Count != 0)
{
var deleteDates = new List<DateTime>();
foreach (var dividendDate in dividendDates)
{
if (tick.Time > dividendDate)
{
DividendsSplits.Add(new CorporateFactorRow(
dividendDate,
previousPriceFactor,
previousSplitFactor,
tick.Value / FinalSplitFactor));
deleteDates.Add(dividendDate);
}
}
dividendDates.RemoveAll(x => deleteDates.Contains(x));
}
if (tick.Time.Month != previousMonth)
{
// Every quarter, try to generate dividend events
if (hasDividends && (tick.Time.Month - 1) % 3 == 0)
{
// Make it so there's a 10% chance that dividends occur if there is no dividend every quarter
if (dividendEveryQuarter || _randomValueGenerator.NextBool(10.0))
{
do
{
previousX += _random.NextDouble() / 10;
previousPriceFactor = (decimal)Math.Tanh(previousX);
} while (previousPriceFactor >= 1.0m || previousPriceFactor <= 0m);
dividendDates.Add(_randomValueGenerator.NextDate(tick.Time, tick.Time.AddMonths(1), (DayOfWeek)_random.Next(1, 5)));
}
}
// Have a 5% chance of a split every month
if (hasSplits && _randomValueGenerator.NextBool(_settings.MonthSplitPercentage))
{
// Produce another split factor that is also bounded by the min and max split factors allowed
if (_randomValueGenerator.NextBool(5.0)) // Add the possibility of a reverse split
{
// A reverse split is a split that is smaller than the current previousSplitFactor
// Update previousSplitFactor with a smaller value that is still bounded below by minPreviousSplitFactor
previousSplitFactor = GetNextPreviousSplitFactor(_random, minPreviousSplitFactor, previousSplitFactor);
}
else
{
// Update previousSplitFactor with a higher value that is still bounded by maxPreviousSplitFactor
// Usually, the split factor tends to grow across the time span(See /Data/Equity/usa/factor_files/aapl for instance)
previousSplitFactor = GetNextPreviousSplitFactor(_random, previousSplitFactor, maxPreviousSplitFactor);
}
splitDates.Add(_randomValueGenerator.NextDate(tick.Time, tick.Time.AddMonths(1), (DayOfWeek)_random.Next(1, 5)));
}
// 10% chance of being renamed every month
if (hasRename && _randomValueGenerator.NextBool(10.0))
{
var randomDate = _randomValueGenerator.NextDate(tick.Time, tick.Time.AddMonths(1), (DayOfWeek)_random.Next(1, 5));
MapRows.Add(new MapFileRow(randomDate, CurrentSymbol.Value));
CurrentSymbol = _symbolGenerator.NextSymbol(_settings.SecurityType, _settings.Market);
}
previousMonth = tick.Time.Month;
monthsTrading++;
}
if (monthsTrading >= 6 && _willBeDelisted && tick.Time > _delistDate)
{
MapRows.Add(new MapFileRow(tick.Time, CurrentSymbol.Value));
break;
}
firstTick = false;
}
}
}
/// <summary>
/// Gets a lower bound that guarantees the FinalSplitFactor, in all the possible
/// cases, will never be smaller than the _minimumFinalSplitFactorAllowed (0.001)
/// </summary>
/// <param name="months">The lower bound for the previous split factor is based on
/// the number of months between the start and end date from ticksHistory <see cref="GenerateSplitsDividends(IEnumerable{Tick})"></param>
/// <returns>A valid lower bound that guarantees the FinalSplitFactor is always higher
/// than the _minimumFinalSplitFactorAllowed</returns>
public static decimal GetLowerBoundForPreviousSplitFactor(int months)
{
return (decimal)(Math.Pow(_minimumFinalSplitFactorAllowed, 1 / (double)(2 * months)));
}
/// <summary>
/// Gets a new valid previousSplitFactor that is still bounded by the given upper and lower
/// bounds
/// </summary>
/// <param name="random">Random number generator</param>
/// <param name="lowerBound">Minimum allowed value to obtain</param>
/// <param name="upperBound">Maximum allowed value to obtain</param>
/// <returns>A new valid previousSplitFactor that is still bounded by the given upper and lower
/// bounds</returns>
public static decimal GetNextPreviousSplitFactor(Random random, decimal lowerBound, decimal upperBound)
{
return ((decimal)random.NextDouble()) * (upperBound - lowerBound) + lowerBound;
}
}
}
@@ -0,0 +1,108 @@
/*
* 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.Linq;
using QuantConnect.Securities.Future;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Generates a new random future <see cref="Symbol"/>. The generates future contract Symbol will have an
/// expiry between the specified time range.
/// </summary>
public class FutureSymbolGenerator : BaseSymbolGenerator
{
private readonly DateTime _minExpiry;
private readonly DateTime _maxExpiry;
private readonly string _market;
public FutureSymbolGenerator(RandomDataGeneratorSettings settings, IRandomValueGenerator random)
: base(settings, random)
{
_minExpiry = settings.Start;
_maxExpiry = settings.End;
_market = settings.Market;
}
/// <summary>
/// Generates a new random future <see cref="Symbol"/>. The generates future contract Symbol will have an
/// expiry between the specified minExpiry and maxExpiry.
/// </summary>
/// <param name="ticker">Optionally can provide a ticker that should be used</param>
/// <returns>A new future contract Symbol with the specified expiration parameters</returns>
protected override IEnumerable<Symbol> GenerateAsset(string ticker = null)
{
if (ticker == null)
{
// get a valid ticker from the Symbol properties database
ticker = NextTickerFromSymbolPropertiesDatabase(SecurityType.Future, _market);
}
var marketHours = MarketHoursDatabase.GetExchangeHours(_market, ticker, SecurityType.Future);
var expiry = GetRandomExpiration(marketHours, _minExpiry, _maxExpiry);
// Try to get the specific expiry function for this future, if available
var symbol = Symbol.CreateFuture(ticker, _market, SecurityIdentifier.DefaultDate);
if (!FuturesExpiryFunctions.FuturesExpiryDictionary.TryGetValue(symbol, out var expiryFunction))
{
// If no expiry function is found, return the future using the previously chosen expiry
yield return Symbol.CreateFuture(ticker, _market, expiry);
yield break;
}
// Get all valid expiries in range using the expiry function
// HashSet ensures unique expiry dates since multiple reference dates may map to same expiry
var validExpiries = new HashSet<DateTime>();
// Extends range by ±1 month to catch all potential expiries.
// Some futures (like NG) calculate expiry based on next month's date
// (e.g., "3 business days before 1st of next month"), so we need to look ahead.
// This buffer ensures we don't miss expiries near range boundaries.
for (var date = _minExpiry.AddMonths(-1); date <= _maxExpiry.AddMonths(1); date = date.AddDays(1))
{
// Calculate expiry date using the futures-specific function
var newExpiry = expiryFunction(date);
// Only include expiries within our target range
if (_minExpiry < newExpiry && newExpiry <= _maxExpiry)
{
// Add to set of valid expiries (automatically handles duplicates)
validExpiries.Add(newExpiry);
}
}
if (validExpiries.Count == 0)
{
yield return Symbol.CreateFuture(ticker, _market, expiry);
yield break;
}
// Randomly select one expiry from the valid set
var skip = Random.NextInt(validExpiries.Count);
expiry = validExpiries.Skip(skip).First();
// Return the future contract using the randomly selected valid expiry
yield return Symbol.CreateFuture(ticker, _market, expiry);
}
/// <summary>
/// There is no limit for the future symbols.
/// </summary>
/// <returns>Returns int.MaxValue</returns>
public override int GetAvailableSymbolCount() => int.MaxValue;
}
}
@@ -0,0 +1,42 @@
/*
* 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;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Defines a type capable of producing random prices
/// </summary>
/// <remarks>
/// Any parameters referenced as a percentage value are always in 'percent space', meaning 1 is 1%.
/// </remarks>
public interface IPriceGenerator
{
/// <summary>
/// Generates an asset price
/// </summary>
/// <param name="maximumPercentDeviation">The maximum percent deviation. This value is in percent space,
/// so a value of 1m is equal to 1%.</param>
/// <param name="referenceDate">date used in price calculation</param>
/// <returns>Returns a new decimal as price</returns>
public decimal NextValue(decimal maximumPercentDeviation, DateTime referenceDate);
/// <summary>
/// Indicates Price generator warmed up and ready to generate new values
/// </summary>
public bool WarmedUp { get; }
}
}
@@ -0,0 +1,84 @@
/*
* 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 QuantConnect.Securities;
using System;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Defines a type capable of producing random values for use in random data generation
/// </summary>
/// <remarks>
/// Any parameters referenced as a percentage value are always in 'percent space', meaning 1 is 1%.
/// </remarks>
public interface IRandomValueGenerator
{
/// <summary>
/// Randomly return a <see cref="bool"/> value with the specified odds of being true
/// </summary>
/// <param name="percentOddsForTrue">The percent odds of being true in percent space, so 10 => 10%</param>
/// <returns>True or false</returns>
bool NextBool(double percentOddsForTrue);
/// <summary>
/// Returns a random floating-point number that is greater than or equal to 0.0, and less than 1.0
/// </summary>
/// <returns>A double-precision floating point number that is greater than or equal to 0.0, and less than 1.0.</returns>
double NextDouble();
/// <summary>
/// Returns a random integer that is within a specified range.
/// </summary>
/// <param name="minValue">the inclusive lower bound of the random number returned</param>
/// <param name="maxValue">the exclusive upper bound of the random number returned</param>
/// <returns>A 32-bit signed integer greater than or equal to minValue and less than maxValue.</returns>
int NextInt(int minValue, int maxValue);
/// <summary>
/// Returns a non-negative random integer that is less than the specified maximum.
/// </summary>
/// <param name="maxValue">the exclusive upper bound of the random number to be generated.</param>
/// <returns>A 32-bit signed integer that is greater than or equal to 0, and less than maxValue.</returns>
int NextInt(int maxValue);
/// <summary>
/// Generates a random <see cref="DateTime"/> between the specified <paramref name="minDateTime"/> and
/// <paramref name="maxDateTime"/>. <paramref name="dayOfWeek"/> is optionally specified to force the
/// result to a particular day of the week
/// </summary>
/// <param name="minDateTime">The minimum date time, inclusive</param>
/// <param name="maxDateTime">The maximum date time, inclusive</param>
/// <param name="dayOfWeek">Optional. The day of week to force</param>
/// <returns>A new <see cref="DateTime"/> within the specified range and optionally of the specified day of week</returns>
DateTime NextDate(DateTime minDateTime, DateTime maxDateTime, DayOfWeek? dayOfWeek);
/// <summary>
/// Generates a random <see cref="decimal"/> suitable as a price. This should observe minimum price
/// variations if available in <see cref="SymbolPropertiesDatabase"/>, and if not, truncating to 2
/// decimal places.
/// </summary>
/// <exception cref="ArgumentException">Throw when the <paramref name="referencePrice"/> or <paramref name="maximumPercentDeviation"/>
/// is less than or equal to zero.</exception>
/// <param name="securityType">The security type the price is being generated for</param>
/// <param name="market">The market of the security the price is being generated for</param>
/// <param name="referencePrice">The reference price used as the mean of random price generation</param>
/// <param name="maximumPercentDeviation">The maximum percent deviation. This value is in percent space,
/// so a value of 1m is equal to 1%.</param>
/// <returns>A new decimal suitable for usage as price within the specified deviation from the reference price</returns>
decimal NextPrice(SecurityType securityType, string market, decimal referencePrice, decimal maximumPercentDeviation);
}
}
@@ -0,0 +1,63 @@
/*
* 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 QuantConnect.Data.Market;
using System.Collections.Generic;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Describes main methods for <see cref="TickGenerator"/>
/// </summary>
public interface ITickGenerator
{
/// <summary>
/// Generates and enumerates data points for current symbol
/// </summary>
IEnumerable<Tick> GenerateTicks();
/// <summary>
/// Generates a random <see cref="Tick"/> that is at most the specified <paramref name="maximumPercentDeviation"/> away from the
/// previous price and is of the requested <paramref name="tickType"/>
/// </summary>
/// <param name="dateTime">The time of the generated tick</param>
/// <param name="tickType">The type of <see cref="Tick"/> to be generated</param>
/// <param name="maximumPercentDeviation">The maximum percentage to deviate from the
/// previous price, for example, 1 would indicate a maximum of 1% deviation from the
/// previous price. For a previous price of 100, this would yield a price between 99 and 101 inclusive</param>
/// <returns>A random <see cref="Tick"/> value that is within the specified <paramref name="maximumPercentDeviation"/>
/// from the previous price</returns>
Tick NextTick(
DateTime dateTime,
TickType tickType,
decimal maximumPercentDeviation
);
/// <summary>
/// Generates a random <see cref="DateTime"/> suitable for use as a tick's emit time.
/// If the density provided is <see cref="DataDensity.Dense"/>, then at least one tick will be generated per <paramref name="resolution"/> step.
/// If the density provided is <see cref="DataDensity.Sparse"/>, then at least one tick will be generated every 5 <paramref name="resolution"/> steps.
/// if the density provided is <see cref="DataDensity.VerySparse"/>, then at least one tick will be generated every 50 <paramref name="resolution"/> steps.
/// Times returned are guaranteed to be within market hours for the specified Symbol
/// </summary>
/// <param name="previous">The previous tick time</param>
/// <param name="resolution">The requested resolution of data</param>
/// <param name="density">The requested data density</param>
/// <returns>A new <see cref="DateTime"/> that is after <paramref name="previous"/> according to the specified <paramref name="resolution"/>
/// and <paramref name="density"/> specified</returns>
DateTime NextTickTime(DateTime previous, Resolution resolution, DataDensity density);
}
}
@@ -0,0 +1,13 @@
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Exception thrown when there are no tickers left to generate for a certain combination of security type and market.
/// </summary>
public class NoTickersAvailableException : RandomValueGeneratorException
{
public NoTickersAvailableException(SecurityType securityType, string market)
: base($"Failed to generate {securityType} symbol for {market}, there are no tickers left")
{
}
}
}
@@ -0,0 +1,74 @@
/*
* 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 QuantConnect.Securities;
using System;
using QuantConnect.Data.Market;
using QuantConnect.Securities.Option;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Pricing model used to determine the fair price or theoretical value for a call or a put option price
/// by default using the Black-Scholes-Merton model
/// </summary>
public class OptionPriceModelPriceGenerator : IPriceGenerator
{
private readonly Option _option;
/// <summary>
/// <see cref="RandomPriceGenerator"/> is always ready to generate new price values as it does not depend on volatility model
/// </summary>
public bool WarmedUp => _option.PriceModel is QLOptionPriceModel optionPriceModel && optionPriceModel.VolatilityEstimatorWarmedUp || _option.PriceModel is not QLOptionPriceModel;
/// <summary>
/// Creates instance of <see cref="OptionPriceModelPriceGenerator"/>
/// </summary>
///<param name="security"><see cref="Security"/> object for which to generate price data</param>
public OptionPriceModelPriceGenerator(Security security)
{
if (security == null)
{
throw new ArgumentNullException(nameof(security), "security cannot be null");
}
if (!security.Symbol.SecurityType.IsOption())
{
throw new ArgumentException($"{nameof(OptionPriceModelPriceGenerator)} model cannot be applied to non-option security.");
}
_option = security as Option;
}
/// <summary>
/// For Black-Scholes-Merton model price calculation relies <see cref="IOptionPriceModel"/> of the security
/// </summary>
/// <param name="maximumPercentDeviation">The maximum percent deviation. This value is in percent space,
/// so a value of 1m is equal to 1%.</param>
/// <param name="referenceDate">current reference date</param>
/// <returns>A new decimal suitable for usage as new security price</returns>
public decimal NextValue(decimal maximumPercentDeviation, DateTime referenceDate)
{
var underlying = _option.Underlying;
var price = underlying.Price;
var tick = new Tick(referenceDate, underlying.Symbol, price, price);
var contract = OptionContract.Create(referenceDate, _option, tick);
var parameters = new OptionPriceModelParameters(_option, null, contract);
return _option.PriceModel.Evaluate(parameters).TheoreticalPrice;
}
}
}
@@ -0,0 +1,103 @@
/*
* 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;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Generates a new random option <see cref="Symbol"/>.
/// </summary>
public class OptionSymbolGenerator : BaseSymbolGenerator
{
private readonly DateTime _minExpiry;
private readonly DateTime _maxExpiry;
private readonly string _market;
private readonly int _symbolChainSize;
private readonly decimal _underlyingPrice;
private readonly decimal _maximumStrikePriceDeviation;
private readonly SecurityType _underlyingSecurityType = SecurityType.Equity;
public OptionSymbolGenerator(RandomDataGeneratorSettings settings, IRandomValueGenerator random, decimal underlyingPrice, decimal maximumStrikePriceDeviation)
: base(settings, random)
{
// We add seven days more because TickGenerator for options needs first three underlying data points to warm up
// the price generator, so if the expiry date is before settings.Start plus three days no quote or trade data is
// generated for this option
_minExpiry = (settings.Start).AddDays(7);
_maxExpiry = (settings.End).AddDays(7);
_market = settings.Market;
_underlyingPrice = underlyingPrice;
_symbolChainSize = settings.ChainSymbolCount;
_maximumStrikePriceDeviation = maximumStrikePriceDeviation;
}
/// <summary>
/// Generates a new random option <see cref="Symbol"/>. The generated option contract Symbol will have an
/// expiry between the specified min and max expiration. The strike
/// price will be within the specified maximum strike price deviation of the underlying symbol price
/// and should be rounded to reasonable value for the given price. For example, a price of 100 dollars would round
/// to 5 dollar increments and a price of 5 dollars would round to 50 cent increments
/// </summary>
/// <param name="ticker">Optionally can provide a ticker that should be used</param>
/// <remarks>
/// Standard contracts expiry on the third Friday.
/// Weekly contracts expiry every week on Friday
/// </remarks>
/// <returns>A new option contract Symbol within the specified expiration and strike price parameters along with its underlying symbol</returns>
protected override IEnumerable<Symbol> GenerateAsset(string ticker = null)
{
// first generate the underlying
var underlying = NextSymbol(_underlyingSecurityType, _market, ticker);
yield return underlying;
var marketHours = MarketHoursDatabase.GetExchangeHours(_market, underlying, _underlyingSecurityType);
var expiry = GetRandomExpiration(marketHours, _minExpiry, _maxExpiry);
var strikes = new HashSet<decimal>();
for (var i = 0; i < _symbolChainSize; i++)
{
decimal strike;
do
{
// generate a random strike while respecting the maximum deviation from the underlying's price
// since these are underlying prices, use Equity as the security type
strike = Random.NextPrice(_underlyingSecurityType, _market, _underlyingPrice,
_maximumStrikePriceDeviation);
// round the strike price to something reasonable
var order = 1 + Math.Log10((double)strike);
strike = strike.RoundToSignificantDigits((int)order);
}
// don't allow duplicate strikes
while (!strikes.Add(strike));
foreach (var optionRight in new [] { OptionRight.Put, OptionRight.Call })
{
// when providing a null option w/ an expiry, it will automatically create the OSI ticker string for the Value
yield return Symbol.CreateOption(underlying, _market, underlying.SecurityType.DefaultOptionStyle(), optionRight, strike, expiry);
}
}
}
/// <summary>
/// Returns the number of symbols with the specified parameters can be generated.
/// There is no limit for the options.
/// </summary>
/// <returns>returns int.MaxValue</returns>
public override int GetAvailableSymbolCount() => int.MaxValue;
}
}
@@ -0,0 +1,334 @@
/*
* 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 QuantConnect.Data;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Data.Market;
using QuantConnect.Securities;
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Lean.Engine.DataFeeds.Enumerators;
using QuantConnect.Logging;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Generates random data according to the specified parameters
/// </summary>
public class RandomDataGenerator
{
private RandomDataGeneratorSettings _settings;
private SecurityManager _securityManager;
/// <summary>
/// Initializes <see cref="RandomDataGenerator"/> instance fields
/// </summary>
/// <param name="settings">random data generation settings</param>
/// <param name="securityManager">security management</param>
public void Init(RandomDataGeneratorSettings settings, SecurityManager securityManager)
{
_settings = settings;
_securityManager = securityManager;
}
/// <summary>
/// Starts data generation
/// </summary>
public void Run()
{
var tickTypesPerSecurityType = SubscriptionManager.DefaultDataTypes();
// can specify a seed value in this ctor if determinism is desired
var random = new Random();
var randomValueGenerator = new RandomValueGenerator();
if (_settings.RandomSeedSet)
{
random = new Random(_settings.RandomSeed);
randomValueGenerator = new RandomValueGenerator(_settings.RandomSeed);
}
var symbolGenerator = BaseSymbolGenerator.Create(_settings, randomValueGenerator);
var maxSymbolCount = symbolGenerator.GetAvailableSymbolCount();
if (_settings.SymbolCount > maxSymbolCount)
{
Log.Error($"RandomDataGenerator.Run(): Limiting Symbol count to {maxSymbolCount}, we don't have more {_settings.SecurityType} tickers for {_settings.Market}");
_settings.SymbolCount = maxSymbolCount;
}
Log.Trace($"RandomDataGenerator.Run(): Begin data generation of {_settings.SymbolCount} randomly generated {_settings.SecurityType} assets...");
// iterate over our randomly generated symbols
var count = 0;
var progress = 0d;
var previousMonth = -1;
foreach (var (symbolRef, currentSymbolGroup) in symbolGenerator.GenerateRandomSymbols()
.GroupBy(s => s.HasUnderlying ? s.Underlying : s)
.Select(g => (g.Key, g.OrderBy(s => s.HasUnderlying).ToList())))
{
Log.Trace($"RandomDataGenerator.Run(): Symbol[{++count}]: {symbolRef} Progress: {progress:0.0}% - Generating data...");
var tickGenerators = new List<IEnumerator<Tick>>();
var tickHistories = new Dictionary<Symbol, List<Tick>>();
Security underlyingSecurity = null;
foreach (var currentSymbol in currentSymbolGroup)
{
if (!_securityManager.TryGetValue(currentSymbol, out var security))
{
security = _securityManager.CreateSecurity(
currentSymbol,
new List<SubscriptionDataConfig>(),
underlying: underlyingSecurity);
_securityManager.Add(security);
}
underlyingSecurity ??= security;
tickGenerators.Add(
new TickGenerator(_settings, tickTypesPerSecurityType[currentSymbol.SecurityType].ToArray(), security, randomValueGenerator)
.GenerateTicks()
.GetEnumerator());
tickHistories.Add(
currentSymbol,
new List<Tick>());
}
using var sync = new SynchronizingBaseDataEnumerator(tickGenerators);
var lastLoggedProgress = 0.0;
Log.Trace("[0%] Initializing tick data generation");
while (sync.MoveNext())
{
var dataPoint = sync.Current;
if (!_securityManager.TryGetValue(dataPoint.Symbol, out var security))
{
Log.Error($"RandomDataGenerator.Run(): Could not find security for symbol {sync.Current.Symbol}");
continue;
}
tickHistories[security.Symbol].Add(dataPoint as Tick);
security.Update(new List<BaseData> { dataPoint }, dataPoint.GetType(), false);
// Calculate and log progress percentage when it increases by more than 3%
var currentProgress = RandomDataGeneratorHelper.GetProgressAsPercentage(_settings.Start, _settings.End, dataPoint.EndTime);
if (currentProgress - lastLoggedProgress >= 3.0)
{
Log.Trace($"[{currentProgress:0.00}%] Generating tick data");
lastLoggedProgress = currentProgress;
}
}
Log.Trace("[100%] Tick data generation completed successfully.");
foreach (var (currentSymbol, tickHistory) in tickHistories)
{
var symbol = currentSymbol;
// This is done so that we can update the Symbol in the case of a rename event
var delistDate = GetDelistingDate(_settings.Start, _settings.End, randomValueGenerator);
var willBeDelisted = randomValueGenerator.NextBool(1.0);
// Companies rarely IPO then disappear within 6 months
if (willBeDelisted && tickHistory.Select(tick => tick.Time.Month).Distinct().Count() <= 6)
{
willBeDelisted = false;
}
var dividendsSplitsMaps = new DividendSplitMapGenerator(
symbol,
_settings,
randomValueGenerator,
symbolGenerator,
random,
delistDate,
willBeDelisted);
// Keep track of renamed symbols and the time they were renamed.
var renamedSymbols = new Dictionary<Symbol, DateTime>();
if (_settings.SecurityType == SecurityType.Equity)
{
dividendsSplitsMaps.GenerateSplitsDividends(tickHistory);
if (!willBeDelisted)
{
dividendsSplitsMaps.DividendsSplits.Add(new CorporateFactorRow(new DateTime(2050, 12, 31), 1m, 1m));
if (dividendsSplitsMaps.MapRows.Count > 1)
{
// Remove the last element if we're going to have a 20501231 entry
dividendsSplitsMaps.MapRows.RemoveAt(dividendsSplitsMaps.MapRows.Count - 1);
}
dividendsSplitsMaps.MapRows.Add(new MapFileRow(new DateTime(2050, 12, 31), dividendsSplitsMaps.CurrentSymbol.Value));
}
// If the Symbol value has changed, update the current Symbol
if (symbol != dividendsSplitsMaps.CurrentSymbol)
{
// Add all Symbol rename events to dictionary
// We skip the first row as it contains the listing event instead of a rename event
foreach (var renameEvent in dividendsSplitsMaps.MapRows.Skip(1))
{
// Symbol.UpdateMappedSymbol does not update the underlying security ID Symbol, which
// is used to create the hash code. Create a new equity Symbol from scratch instead.
symbol = Symbol.Create(renameEvent.MappedSymbol, SecurityType.Equity, _settings.Market);
renamedSymbols.Add(symbol, renameEvent.Date);
Log.Trace($"RandomDataGenerator.Run(): Symbol[{count}]: {symbol} will be renamed on {renameEvent.Date}");
}
}
else
{
// This ensures that ticks will be written for the current Symbol up until 9999-12-31
renamedSymbols.Add(symbol, new DateTime(9999, 12, 31));
}
symbol = dividendsSplitsMaps.CurrentSymbol;
// Write Splits and Dividend events to directory factor_files
var factorFile = new CorporateFactorProvider(symbol.Value, dividendsSplitsMaps.DividendsSplits, _settings.Start);
var mapFile = new MapFile(symbol.Value, dividendsSplitsMaps.MapRows);
factorFile.WriteToFile(symbol);
mapFile.WriteToCsv(_settings.Market, symbol.SecurityType);
Log.Trace($"RandomDataGenerator.Run(): Symbol[{count}]: {symbol} Dividends, splits, and map files have been written to disk.");
}
else
{
// This ensures that ticks will be written for the current Symbol up until 9999-12-31
renamedSymbols.Add(symbol, new DateTime(9999, 12, 31));
}
// define aggregators via settings
var aggregators = CreateAggregators(_settings, tickTypesPerSecurityType[currentSymbol.SecurityType].ToArray()).ToList();
Symbol previousSymbol = null;
var currentCount = 0;
var monthsTrading = 0;
foreach (var renamed in renamedSymbols)
{
var previousRenameDate = previousSymbol == null ? new DateTime(1, 1, 1) : renamedSymbols[previousSymbol];
var previousRenameDateDay = new DateTime(previousRenameDate.Year, previousRenameDate.Month, previousRenameDate.Day);
var renameDate = renamed.Value;
var renameDateDay = new DateTime(renameDate.Year, renameDate.Month, renameDate.Day);
foreach (var tick in tickHistory.Where(tick => tick.Time >= previousRenameDate && previousRenameDateDay != TickDay(tick)))
{
// Prevents the aggregator from being updated with ticks after the rename event
if (TickDay(tick) > renameDateDay)
{
break;
}
if (tick.Time.Month != previousMonth)
{
Log.Trace($"RandomDataGenerator.Run(): Symbol[{count}]: Month: {tick.Time:MMMM}");
previousMonth = tick.Time.Month;
monthsTrading++;
}
foreach (var item in aggregators)
{
tick.Value = tick.Value / dividendsSplitsMaps.FinalSplitFactor;
item.Consolidator.Update(tick);
}
if (monthsTrading >= 6 && willBeDelisted && tick.Time > delistDate)
{
Log.Trace($"RandomDataGenerator.Run(): Symbol[{count}]: {renamed.Key} delisted at {tick.Time:MMMM yyyy}");
break;
}
}
// count each stage as a point, so total points is 2*Symbol-count
// and the current progress is twice the current, but less one because we haven't finished writing data yet
progress = 100 * (2 * count - 1) / (2.0 * _settings.SymbolCount);
Log.Trace($"RandomDataGenerator.Run(): Symbol[{count}]: {renamed.Key} Progress: {progress:0.0}% - Saving data in LEAN format");
// persist consolidated data to disk
foreach (var item in aggregators)
{
var writer = new LeanDataWriter(item.Resolution, renamed.Key, Globals.DataFolder, item.TickType);
// send the flushed data into the writer. pulling the flushed list is very important,
// lest we likely wouldn't get the last piece of data stuck in the consolidator
// Filter out the data we're going to write here because filtering them in the consolidator update phase
// makes it write all dates for some unknown reason
writer.Write(item.Flush().Where(data => data.Time > previousRenameDate && previousRenameDateDay != DataDay(data)));
}
// update progress
progress = 100 * (2 * count) / (2.0 * _settings.SymbolCount);
Log.Trace($"RandomDataGenerator.Run(): Symbol[{count}]: {symbol} Progress: {progress:0.0}% - Symbol data generation and output completed");
previousSymbol = renamed.Key;
currentCount++;
}
}
}
Log.Trace("RandomDataGenerator.Run(): Random data generation has completed.");
DateTime TickDay(Tick tick) => new(tick.Time.Year, tick.Time.Month, tick.Time.Day);
DateTime DataDay(BaseData data) => new(data.Time.Year, data.Time.Month, data.Time.Day);
}
public static DateTime GetDateMidpoint(DateTime start, DateTime end)
{
TimeSpan span = end.Subtract(start);
int span_time = (int)span.TotalMinutes;
double diff_span = -(span_time / 2.0);
DateTime start_time = end.AddMinutes(Math.Round(diff_span, 2, MidpointRounding.ToEven));
//Returns a DateTime object that is halfway between start and end
return start_time;
}
public static DateTime GetDelistingDate(DateTime start, DateTime end, RandomValueGenerator randomValueGenerator)
{
var mid_point = GetDateMidpoint(start, end);
var delist_Date = randomValueGenerator.NextDate(mid_point, end, null);
//Returns a DateTime object that is a random value between the mid_point and end
return delist_Date;
}
public static IEnumerable<TickAggregator> CreateAggregators(RandomDataGeneratorSettings settings, TickType[] tickTypes)
{
// create default aggregators for tick type/resolution
foreach (var tickAggregator in TickAggregator.ForTickTypes(settings.SecurityType, settings.Resolution, tickTypes))
{
yield return tickAggregator;
}
// ensure we have a daily consolidator when coarse is enabled
if (settings.IncludeCoarse && settings.Resolution != Resolution.Daily)
{
// prefer trades for coarse - in practice equity only does trades, but leaving this as configurable
if (tickTypes.Contains(TickType.Trade))
{
yield return TickAggregator.ForTickTypes(settings.SecurityType, Resolution.Daily, TickType.Trade).Single();
}
else
{
yield return TickAggregator.ForTickTypes(settings.SecurityType, Resolution.Daily, TickType.Quote).Single();
}
}
}
}
}
@@ -0,0 +1,38 @@
/*
* 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;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Provides helper methods for the Random Data Generator
/// </summary>
public static class RandomDataGeneratorHelper
{
/// <summary>
/// Calculates the progress percentage of the current time between a start and end time.
/// </summary>
/// <param name="start">The start time of the process.</param>
/// <param name="end">The end time of the process.</param>
/// <param name="currentTime">The current time to evaluate progress.</param>
/// <returns>The progress as a percentage, rounded to two decimal places.</returns>
public static double GetProgressAsPercentage(DateTime start, DateTime end, DateTime currentTime)
{
var totalDuration = end - start;
return Math.Round((currentTime - start).TotalMilliseconds * 1.0 / totalDuration.TotalMilliseconds * 100, 2);
}
}
}
@@ -0,0 +1,134 @@
/*
* 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 QuantConnect.Configuration;
using QuantConnect.Data.Auxiliary;
using QuantConnect.Data.Market;
using QuantConnect.Interfaces;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using QuantConnect.ToolBox.CoarseUniverseGenerator;
using QuantConnect.Util;
using System;
using System.Collections.Generic;
using QuantConnect.Logging;
using QuantConnect.Data;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Creates and starts <see cref="RandomDataGenerator"/> instance
/// </summary>
public static class RandomDataGeneratorProgram
{
private static readonly IRiskFreeInterestRateModel _interestRateProvider = new InterestRateProvider();
public static void RandomDataGenerator(
string startDateString,
string endDateString,
string symbolCountString,
string market,
string securityTypeString,
string resolutionString,
string dataDensityString,
string includeCoarseString,
string quoteTradeRatioString,
string randomSeed,
string hasIpoPercentageString,
string hasRenamePercentageString,
string hasSplitsPercentageString,
string hasDividendsPercentageString,
string dividendEveryQuarterPercentageString,
string optionPriceEngineName,
string volatilityModelResolutionString,
string chainSymbolCountString,
List<string> tickers
)
{
var settings = RandomDataGeneratorSettings.FromCommandLineArguments(
startDateString,
endDateString,
symbolCountString,
market,
securityTypeString,
resolutionString,
dataDensityString,
includeCoarseString,
quoteTradeRatioString,
randomSeed,
hasIpoPercentageString,
hasRenamePercentageString,
hasSplitsPercentageString,
hasDividendsPercentageString,
dividendEveryQuarterPercentageString,
optionPriceEngineName,
volatilityModelResolutionString,
chainSymbolCountString,
tickers
);
if (settings.Start.Year < 1998)
{
Log.Error($"RandomDataGeneratorProgram(): Required parameter --start must be at least 19980101");
Environment.Exit(1);
}
var securityManager = new SecurityManager(new TimeKeeper(settings.Start, new[] { TimeZones.Utc }));
var securityService = new SecurityService(
new CashBook(),
MarketHoursDatabase.FromDataFolder(),
SymbolPropertiesDatabase.FromDataFolder(),
new SecurityInitializerProvider(new FuncSecurityInitializer(security =>
{
// init price
security.SetMarketPrice(new Tick(settings.Start, security.Symbol, 100, 100));
security.SetMarketPrice(new OpenInterest(settings.Start, security.Symbol, 10000));
// from settings
security.VolatilityModel = new StandardDeviationOfReturnsVolatilityModel(settings.VolatilityModelResolution);
// from settings
if (security is Option option)
{
option.PriceModel = OptionPriceModels.QuantLib.Create(settings.OptionPriceEngineName,
_interestRateProvider.GetRiskFreeRate(settings.Start, settings.End));
}
})),
RegisteredSecurityDataTypesProvider.Null,
new SecurityCacheProvider(
new SecurityPortfolioManager(securityManager, new SecurityTransactionManager(null, securityManager), new AlgorithmSettings())),
new MapFilePrimaryExchangeProvider(Composer.Instance.GetExportedValueByTypeName<IMapFileProvider>(Config.Get("map-file-provider", "LocalDiskMapFileProvider")))
);
securityManager.SetSecurityService(securityService);
var generator = new RandomDataGenerator();
generator.Init(settings, securityManager);
generator.Run();
if (settings.IncludeCoarse && settings.SecurityType == SecurityType.Equity)
{
Log.Trace("RandomDataGeneratorProgram(): Launching coarse data generator...");
CoarseUniverseGeneratorProgram.CoarseUniverseGenerator();
}
if (!Console.IsInputRedirected)
{
Log.Trace("RandomDataGeneratorProgram(): Press any key to exit...");
Console.ReadKey();
}
}
}
}
@@ -0,0 +1,337 @@
/*
* 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 QuantConnect.Brokerages;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using QuantConnect.Logging;
using QuantConnect.Util;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
public class RandomDataGeneratorSettings
{
private static int MarketCode = 100;
private static readonly string[] DateFormats = { DateFormat.EightCharacter, DateFormat.YearMonth, "yyyy-MM-dd" };
public DateTime Start { get; init; }
public DateTime End { get; init; }
public SecurityType SecurityType { get; init; } = SecurityType.Equity;
public DataDensity DataDensity { get; init; } = DataDensity.Dense;
public Resolution Resolution { get; init; } = Resolution.Minute;
public string Market { get; init; }
public bool IncludeCoarse { get; init; } = true;
public int SymbolCount { get; set; }
public double QuoteTradeRatio { get; init; } = 1;
public int RandomSeed { get; init; }
public bool RandomSeedSet { get; init; }
public double HasIpoPercentage { get; init; }
public double HasRenamePercentage { get; init; }
public double HasSplitsPercentage { get; init; }
public double MonthSplitPercentage { get; init; }
public double HasDividendsPercentage { get; init; }
public double DividendEveryQuarterPercentage { get; init; }
public string OptionPriceEngineName { get; init; }
public int ChainSymbolCount { get; init; } = 1;
public Resolution VolatilityModelResolution { get; init; } = Resolution.Daily;
public List<string> Tickers { get; init; }
public static RandomDataGeneratorSettings FromCommandLineArguments(
string startDateString,
string endDateString,
string symbolCountString,
string market,
string securityTypeString,
string resolutionString,
string dataDensityString,
string includeCoarseString,
string quoteTradeRatioString,
string randomSeedString,
string hasIpoPercentageString,
string hasRenamePercentageString,
string hasSplitsPercentageString,
string hasDividendsPercentageString,
string dividendEveryQuarterPercentageString,
string optionPriceEngineName,
string volatilityModelResolutionString,
string chainSymbolCountString,
List<string> tickers,
double monthSplitPercentage = 5.0
)
{
var randomSeedSet = true;
int randomSeed;
int symbolCount;
int chainSymbolCount;
bool includeCoarse;
Resolution resolution;
double quoteTradeRatio;
DataDensity dataDensity;
SecurityType securityType;
DateTime startDate, endDate;
double hasIpoPercentage;
double hasRenamePercentage;
double hasSplitsPercentage;
double hasDividendsPercentage;
double dividendEveryQuarterPercentage;
Resolution volatilityModelResolution;
var failed = false;
// --start
if (!DateTime.TryParseExact(startDateString, DateFormats, null, DateTimeStyles.None, out startDate))
{
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Required parameter --from-date was incorrectly formatted. Please specify in yyyyMMdd format. Value provided: '{startDateString}'");
}
// --end
if (!DateTime.TryParseExact(endDateString, DateFormats, null, DateTimeStyles.None, out endDate))
{
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Required parameter --to-date was incorrectly formatted. Please specify in yyyyMMdd format. Value provided: '{endDateString}'");
}
// --tickers
if (!tickers.IsNullOrEmpty())
{
symbolCount = tickers.Count;
Log.Trace("RandomDataGeneratorSettings(): Ignoring symbol count will use provided tickers");
}
// --symbol-count
else if (!int.TryParse(symbolCountString, out symbolCount) || symbolCount <= 0)
{
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Required parameter --symbol-count was incorrectly formatted. Please specify a valid integer greater than zero. Value provided: '{symbolCountString}'");
}
// --chain-symbol-count
if (!int.TryParse(chainSymbolCountString, out chainSymbolCount) || chainSymbolCount <= 0)
{
chainSymbolCount = 10;
Log.Trace($"RandomDataGeneratorSettings(): Using default value of '{chainSymbolCount}' for --chain-symbol-count");
}
// --resolution
if (string.IsNullOrEmpty(resolutionString))
{
resolution = Resolution.Minute;
Log.Trace($"RandomDataGeneratorSettings(): Using default value of '{resolution}' for --resolution");
}
else if (!Enum.TryParse(resolutionString, true, out resolution))
{
var validValues = string.Join(", ", Enum.GetValues(typeof(Resolution)).Cast<Resolution>());
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Optional parameter --resolution was incorrectly formatted. Default is Minute. Please specify a valid Resolution. Value provided: '{resolutionString}' Valid values: {validValues}");
}
// --standard deviation volatility period span
if (string.IsNullOrEmpty(volatilityModelResolutionString))
{
volatilityModelResolution = Resolution.Daily;
Log.Trace($"RandomDataGeneratorSettings():Using default value of '{resolution}' for --resolution");
}
else if (!Enum.TryParse(volatilityModelResolutionString, true, out volatilityModelResolution))
{
var validValues = string.Join(", ", Enum.GetValues(typeof(Resolution)).Cast<Resolution>());
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Optional parameter --volatility-model-resolution was incorrectly formatted. Default is Daily. Please specify a valid Resolution. Value provided: '{volatilityModelResolutionString}' Valid values: {validValues}");
}
// --security-type
if (string.IsNullOrEmpty(securityTypeString))
{
securityType = SecurityType.Equity;
Log.Trace($"RandomDataGeneratorSettings(): Using default value of '{securityType}' for --security-type");
}
else if (!Enum.TryParse(securityTypeString, true, out securityType))
{
var validValues = string.Join(", ", Enum.GetValues(typeof(SecurityType)).Cast<SecurityType>());
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Optional parameter --security-type is invalid. Default is Equity. Please specify a valid SecurityType. Value provided: '{securityTypeString}' Valid values: {validValues}");
}
if (securityType == SecurityType.Option && resolution != Resolution.Minute)
{
failed = true;
Log.Error($"RandomDataGeneratorSettings(): When using --security-type=Option you must specify --resolution=Minute");
}
// --market
if (string.IsNullOrEmpty(market))
{
market = DefaultBrokerageModel.DefaultMarketMap[securityType];
Log.Trace($"RandomDataGeneratorSettings(): Using default value of '{market}' for --market and --security-type={securityType}");
}
else if (QuantConnect.Market.Encode(market) == null)
{
// be sure to add a reference to the unknown market, otherwise we won't be able to decode it coming out
QuantConnect.Market.Add(market, Interlocked.Increment(ref MarketCode));
Log.Trace($"RandomDataGeneratorSettings(): Please verify that the specified market value is correct: '{market}' This value is not known has been added to the market value map. If this is an error, stop the application immediately using Ctrl+C");
}
// --include-coarse
if (string.IsNullOrEmpty(includeCoarseString))
{
includeCoarse = securityType == SecurityType.Equity;
if (securityType != SecurityType.Equity)
{
Log.Trace($"RandomDataGeneratorSettings(): Using default value of '{includeCoarse}' for --security-type={securityType}");
}
}
else if (!bool.TryParse(includeCoarseString, out includeCoarse))
{
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Optional parameter --include-coarse was incorrectly formatted. Please specify a valid boolean. Value provided: '{includeCoarseString}'. Valid values: 'true' or 'false'");
}
else if (includeCoarse && securityType != SecurityType.Equity)
{
Log.Trace("RandomDataGeneratorSettings(): Optional parameter --include-coarse will be ignored because it only applies to --security-type=Equity");
}
// --data-density
if (string.IsNullOrEmpty(dataDensityString))
{
dataDensity = DataDensity.Dense;
if (securityType == SecurityType.Option)
{
dataDensity = DataDensity.Sparse;
}
Log.Trace($"RandomDataGeneratorSettings(): Using default value of '{dataDensity}' for --data-density");
}
else if (!Enum.TryParse(dataDensityString, true, out dataDensity))
{
var validValues = string.Join(", ", Enum.GetValues(typeof(DataDensity))).Cast<DataDensity>();
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Optional parameter --data-density was incorrectly formatted. Please specify a valid DataDensity. Value provided: '{dataDensityString}'. Valid values: {validValues}");
}
// --quote-trade-ratio
if (string.IsNullOrEmpty(quoteTradeRatioString))
{
quoteTradeRatio = 1;
}
else if (!double.TryParse(quoteTradeRatioString, out quoteTradeRatio))
{
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Optional parameter --quote-trade-ratio was incorrectly formatted. Please specify a valid double greater than or equal to zero. Value provided: '{quoteTradeRatioString}'");
}
// --random-seed
if (string.IsNullOrEmpty(randomSeedString))
{
randomSeed = 0;
randomSeedSet = false;
}
else if (!int.TryParse(randomSeedString, out randomSeed))
{
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Optional parameter --random-seed was incorrectly formatted. Please specify a valid integer");
}
// --ipo-percentage
if (string.IsNullOrEmpty(hasIpoPercentageString))
{
hasIpoPercentage = 5.0;
}
else if (!double.TryParse(hasIpoPercentageString, out hasIpoPercentage))
{
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Optional parameter --ipo-percentage was incorrectly formatted. Please specify a valid double greater than or equal to zero. Value provided: '{hasIpoPercentageString}'");
}
// --rename-percentage
if (string.IsNullOrEmpty(hasRenamePercentageString))
{
hasRenamePercentage = 30.0;
}
else if (!double.TryParse(hasRenamePercentageString, out hasRenamePercentage))
{
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Optional parameter --rename-percentage was incorrectly formatted. Please specify a valid double greater than or equal to zero. Value provided: '{hasRenamePercentageString}'");
}
// --splits-percentage
if (string.IsNullOrEmpty(hasSplitsPercentageString))
{
hasSplitsPercentage = 15.0;
}
else if (!double.TryParse(hasSplitsPercentageString, out hasSplitsPercentage))
{
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Optional parameter --splits-percentage was incorrectly formatted. Please specify a valid double greater than or equal to zero. Value provided: '{hasSplitsPercentageString}'");
}
// --dividends-percentage
if (string.IsNullOrEmpty(hasDividendsPercentageString))
{
hasDividendsPercentage = 60.0;
}
else if (!double.TryParse(hasDividendsPercentageString, out hasDividendsPercentage))
{
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Optional parameter --dividends-percentage was incorrectly formatted. Please specify a valid double greater than or equal to zero. Value provided: '{hasDividendsPercentageString}'");
}
// --dividend-every-quarter-percentage
if (string.IsNullOrEmpty(dividendEveryQuarterPercentageString))
{
dividendEveryQuarterPercentage = 30.0;
}
else if (!double.TryParse(dividendEveryQuarterPercentageString, out dividendEveryQuarterPercentage))
{
failed = true;
Log.Error($"RandomDataGeneratorSettings(): Optional parameter --dividend-ever-quarter-percentage was incorrectly formatted. Please specify a valid double greater than or equal to zero. Value provided: '{dividendEveryQuarterPercentageString}'");
}
if (failed)
{
Log.Error("RandomDataGeneratorSettings(): Please address the errors and run the application again.");
Environment.Exit(-1);
}
return new RandomDataGeneratorSettings
{
End = endDate,
Start = startDate,
Market = market,
SymbolCount = symbolCount,
SecurityType = securityType,
QuoteTradeRatio = quoteTradeRatio,
ChainSymbolCount = chainSymbolCount,
Resolution = resolution,
DataDensity = dataDensity,
IncludeCoarse = includeCoarse,
RandomSeed = randomSeed,
RandomSeedSet = randomSeedSet,
HasIpoPercentage = hasIpoPercentage,
HasRenamePercentage = hasRenamePercentage,
HasSplitsPercentage = hasSplitsPercentage,
MonthSplitPercentage = monthSplitPercentage,
HasDividendsPercentage = hasDividendsPercentage,
DividendEveryQuarterPercentage = dividendEveryQuarterPercentage,
OptionPriceEngineName = optionPriceEngineName,
VolatilityModelResolution = volatilityModelResolution,
Tickers = tickers
};
}
}
}
@@ -0,0 +1,55 @@
/*
* 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 QuantConnect.Securities;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Random pricing model used to determine the fair price or theoretical value for a call or a put option
/// </summary>
public class RandomPriceGenerator : IPriceGenerator
{
private readonly Security _security;
private readonly IRandomValueGenerator _random;
/// <summary>
/// Creates instance of <see cref="RandomPriceGenerator"/>
/// </summary>
///<param name="security"><see cref="Security"/> object for which to generate price data</param>
/// <param name="random"><see cref="IRandomValueGenerator"/> type capable of producing random values</param>
public RandomPriceGenerator(Security security, IRandomValueGenerator random)
{
_security = security;
_random = random;
}
/// <summary>
/// <see cref="RandomPriceGenerator"/> is always ready to generate new price values as it does not depend on volatility model
/// </summary>
public bool WarmedUp => true;
/// <summary>
/// Generates an asset price
/// </summary>
/// <param name="maximumPercentDeviation">The maximum percent deviation. This value is in percent space,
/// so a value of 1m is equal to 1%.</param>
/// <param name="referenceDate">date used in price calculation</param>
/// <returns>Returns a new decimal as price</returns>
public decimal NextValue(decimal maximumPercentDeviation, DateTime referenceDate)
=> _random.NextPrice(_security.Symbol.SecurityType, _security.Symbol.ID.Market, _security.Price, maximumPercentDeviation);
}
}
@@ -0,0 +1,231 @@
/*
* 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 QuantConnect.Securities;
using QuantConnect.Util;
using System;
using System.Linq;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Provides an implementation of <see cref="IRandomValueGenerator"/> that uses
/// <see cref="Random"/> to generate random values
/// </summary>
public class RandomValueGenerator : IRandomValueGenerator
{
private readonly Random _random;
private readonly MarketHoursDatabase _marketHoursDatabase;
private readonly SymbolPropertiesDatabase _symbolPropertiesDatabase;
private const decimal _maximumPriceAllowed = 1000000m;
public RandomValueGenerator()
: this(new Random())
{ }
public RandomValueGenerator(int seed)
: this(new Random(seed))
{ }
public RandomValueGenerator(Random random)
: this(random, MarketHoursDatabase.FromDataFolder(), SymbolPropertiesDatabase.FromDataFolder())
{ }
public RandomValueGenerator(
int seed,
MarketHoursDatabase marketHoursDatabase,
SymbolPropertiesDatabase symbolPropertiesDatabase
)
: this(new Random(seed), marketHoursDatabase, symbolPropertiesDatabase)
{ }
public RandomValueGenerator(Random random, MarketHoursDatabase marketHoursDatabase, SymbolPropertiesDatabase symbolPropertiesDatabase)
{
_random = random;
_marketHoursDatabase = marketHoursDatabase;
_symbolPropertiesDatabase = symbolPropertiesDatabase;
}
public bool NextBool(double percentOddsForTrue)
{
return _random.NextDouble() <= percentOddsForTrue / 100;
}
public virtual DateTime NextDate(DateTime minDateTime, DateTime maxDateTime, DayOfWeek? dayOfWeek)
{
if (maxDateTime < minDateTime)
{
throw new ArgumentException(
"The maximum date time must be less than or equal to the minimum date time specified"
);
}
// compute a random date time value
var rangeInDays = (int)maxDateTime.Subtract(minDateTime).TotalDays;
var daysOffsetFromMin = _random.Next(0, rangeInDays);
var dateTime = minDateTime.AddDays(daysOffsetFromMin);
var currentDayOfWeek = dateTime.DayOfWeek;
if (!dayOfWeek.HasValue || currentDayOfWeek == dayOfWeek.Value)
{
// either DOW wasn't specified or we got REALLY lucky, although, I suppose it'll happen 1/7 (~14%) of the time
return dateTime;
}
var nextDayOfWeek = Enumerable.Range(0, 7)
.Select(i => dateTime.AddDays(i))
.First(dt => dt.DayOfWeek == dayOfWeek.Value);
var previousDayOfWeek = Enumerable.Range(0, 7)
.Select(i => dateTime.AddDays(-i))
.First(dt => dt.DayOfWeek == dayOfWeek.Value);
// both are valid dates, so chose one randomly
if (IsWithinRange(nextDayOfWeek, minDateTime, maxDateTime) &&
IsWithinRange(previousDayOfWeek, minDateTime, maxDateTime)
)
{
return _random.Next(0, 1) == 0
? previousDayOfWeek
: nextDayOfWeek;
}
if (IsWithinRange(nextDayOfWeek, minDateTime, maxDateTime))
{
return nextDayOfWeek;
}
if (IsWithinRange(previousDayOfWeek, minDateTime, maxDateTime))
{
return previousDayOfWeek;
}
throw new ArgumentException("The provided min and max dates do not have the requested day of week between them");
}
public double NextDouble() => _random.NextDouble();
public int NextInt(int minValue, int maxValue) => _random.Next(minValue, maxValue);
public int NextInt(int maxValue) => _random.Next(maxValue);
/// <summary>
/// Generates a random <see cref="decimal"/> suitable as a price. This should observe minimum price
/// variations if available in <see cref="SymbolPropertiesDatabase"/>, and if not, truncating to 2
/// decimal places.
/// </summary>
/// <exception cref="ArgumentException">Throw when the <paramref name="referencePrice"/> or <paramref name="maximumPercentDeviation"/>
/// is less than or equal to zero.</exception>
/// <param name="securityType">The security type the price is being generated for</param>
/// <param name="market">The market of the security the price is being generated for</param>
/// <param name="referencePrice">The reference price used as the mean of random price generation</param>
/// <param name="maximumPercentDeviation">The maximum percent deviation. This value is in percent space,
/// so a value of 1m is equal to 1%.</param>
/// <returns>A new decimal suitable for usage as price within the specified deviation from the reference price</returns>
public virtual decimal NextPrice(SecurityType securityType, string market, decimal referencePrice, decimal maximumPercentDeviation)
{
if (referencePrice <= 0)
{
if (securityType == SecurityType.Option && referencePrice == 0)
{
return 0;
}
throw new ArgumentException("The provided reference price must be a positive number.");
}
if (maximumPercentDeviation <= 0)
{
throw new ArgumentException("The provided maximum percent deviation must be a positive number");
}
// convert from percent space to decimal space
maximumPercentDeviation /= 100m;
var symbolProperties = _symbolPropertiesDatabase.GetSymbolProperties(market, null, securityType, "USD");
var minimumPriceVariation = symbolProperties.MinimumPriceVariation;
decimal price;
var attempts = 0;
var increaseProbabilityFactor = 0.5;
do
{
// what follows is a simple model of browning motion that
// limits the walk to the specified percent deviation
var deviation = referencePrice * maximumPercentDeviation * (decimal)(NextDouble() - increaseProbabilityFactor);
deviation = Math.Sign(deviation) * Math.Max(Math.Abs(deviation), minimumPriceVariation);
price = referencePrice + deviation;
price = RoundPrice(price, minimumPriceVariation);
if (price < 20 * minimumPriceVariation)
{
// The price should not be to close to the minimum price variation.
// Invalidate the price to try again and increase the probability of it to going up
price = -1m;
increaseProbabilityFactor = Math.Max(increaseProbabilityFactor - 0.05, 0);
}
if (price > (_maximumPriceAllowed / 10m))
{
// The price should not be too higher
// Decrease the probability of it to going up
increaseProbabilityFactor = increaseProbabilityFactor + 0.05;
}
if (price > _maximumPriceAllowed)
{
// The price should not be too higher
// Invalidate the price to try again
price = -1;
}
} while (!IsPriceValid(securityType, price) && ++attempts < 10);
if (!IsPriceValid(securityType, price))
{
// if still invalid, use the last price
price = referencePrice;
}
return price;
}
private static decimal RoundPrice(decimal price, decimal minimumPriceVariation)
{
if (minimumPriceVariation == 0) return minimumPriceVariation;
return Math.Round(price / minimumPriceVariation) * minimumPriceVariation;
}
private bool IsWithinRange(DateTime value, DateTime min, DateTime max)
{
return value >= min && value <= max;
}
private static bool IsPriceValid(SecurityType securityType, decimal price)
{
switch (securityType)
{
case SecurityType.Option:
{
return price >= 0;
}
default:
{
return price > 0 && price < _maximumPriceAllowed;
}
}
}
}
}
@@ -0,0 +1,15 @@
using System;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Provides a base class for exceptions thrown by implementations of <see cref="IRandomValueGenerator"/>
/// </summary>
public class RandomValueGeneratorException : ApplicationException
{
public RandomValueGeneratorException(string message)
: base(message)
{
}
}
}
@@ -0,0 +1,30 @@
/*
* 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 QuantConnect.Interfaces;
using QuantConnect.Securities;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
public class SecurityInitializerProvider : ISecurityInitializerProvider
{
public ISecurityInitializer SecurityInitializer { get; }
public SecurityInitializerProvider(ISecurityInitializer securityInitializer)
{
SecurityInitializer = securityInitializer;
}
}
}
@@ -0,0 +1,281 @@
/*
* 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 QuantConnect.Data.Market;
using QuantConnect.Securities;
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Logging;
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Generates random tick data according to the settings provided
/// </summary>
public class TickGenerator : ITickGenerator
{
private readonly IPriceGenerator _priceGenerator;
private Symbol Symbol => Security.Symbol;
private readonly IRandomValueGenerator _random;
private readonly RandomDataGeneratorSettings _settings;
private readonly TickType[] _tickTypes;
private MarketHoursDatabase MarketHoursDatabase { get; }
private SymbolPropertiesDatabase SymbolPropertiesDatabase { get; }
private Security Security { get; }
public TickGenerator(RandomDataGeneratorSettings settings, TickType[] tickTypes, Security security, IRandomValueGenerator random)
{
_random = random;
_settings = settings;
_tickTypes = tickTypes;
Security = security;
SymbolPropertiesDatabase = SymbolPropertiesDatabase.FromDataFolder();
MarketHoursDatabase = MarketHoursDatabase.FromDataFolder();
if (Symbol.SecurityType.IsOption())
{
_priceGenerator = new OptionPriceModelPriceGenerator(security);
}
else
{
_priceGenerator = new RandomPriceGenerator(security, random);
}
}
public IEnumerable<Tick> GenerateTicks()
{
var current = _settings.Start;
// There is a possibility that even though this succeeds, the DateTime
// generated may be the same as the starting DateTime, although the probability
// of this happening diminishes the longer the period we're generating data for is
if (_random.NextBool(_settings.HasIpoPercentage))
{
current = _random.NextDate(_settings.Start, _settings.End, null);
Log.Trace($"\tSymbol: {Symbol} has delayed IPO at date {current:yyyy MMMM dd}");
}
// creates a max deviation that scales parabolically as resolution decreases (lower frequency)
var deviation = GetMaximumDeviation(_settings.Resolution);
while (current <= _settings.End)
{
var next = NextTickTime(current, _settings.Resolution, _settings.DataDensity);
// The current date can be the last one of the last day before the market closes
// so the next date could be beyond de end date
if (next > _settings.End)
{
break;
}
if (_tickTypes.Contains(TickType.OpenInterest))
{
if (next.Date != current.Date)
{
// 5% deviation in daily OI
var openInterest = NextTick(next.Date, TickType.OpenInterest, 5m);
yield return openInterest;
}
}
Tick nextTick = null;
// keeps quotes close to the trades for consistency
if (_tickTypes.Contains(TickType.Trade) &&
_tickTypes.Contains(TickType.Quote))
{
// %odds of getting a trade tick, for example, a quote:trade ratio of 2 means twice as likely
// to get a quote, which means you have a 33% chance of getting a trade => 1/3
var tradeChancePercent = 100 / (1 + _settings.QuoteTradeRatio);
nextTick = NextTick(
next,
_random.NextBool(tradeChancePercent)
? TickType.Trade
: TickType.Quote,
deviation);
}
else if (_tickTypes.Contains(TickType.Trade))
{
nextTick = NextTick(next, TickType.Trade, deviation);
}
else if (_tickTypes.Contains(TickType.Quote))
{
nextTick = NextTick(next, TickType.Quote, deviation);
}
if (nextTick != null && _priceGenerator.WarmedUp)
{
yield return nextTick;
}
// advance to the next time step
current = next;
}
}
/// <summary>
/// Generates a random <see cref="Tick"/> that is at most the specified <paramref name="maximumPercentDeviation"/> away from the
/// previous price and is of the requested <paramref name="tickType"/>
/// </summary>
/// <param name="dateTime">The time of the generated tick</param>
/// <param name="tickType">The type of <see cref="Tick"/> to be generated</param>
/// <param name="maximumPercentDeviation">The maximum percentage to deviate from the
/// previous price for example, 1 would indicate a maximum of 1% deviation from the
/// previous price. For a previous price of 100, this would yield a price between 99 and 101 inclusive</param>
/// <returns>A random <see cref="Tick"/> value that is within the specified <paramref name="maximumPercentDeviation"/>
/// from the previous price</returns>
public virtual Tick NextTick(DateTime dateTime, TickType tickType, decimal maximumPercentDeviation)
{
var next = _priceGenerator.NextValue(maximumPercentDeviation, dateTime);
var tick = new Tick
{
Time = dateTime,
Symbol = Symbol,
TickType = tickType,
Value = next
};
switch (tickType)
{
case TickType.OpenInterest:
return NextOpenInterest(dateTime, Security.OpenInterest, maximumPercentDeviation);
case TickType.Trade:
tick.Quantity = _random.NextInt(1, 1500);
return tick;
case TickType.Quote:
var bid = _random.NextPrice(Symbol.SecurityType, Symbol.ID.Market, tick.Value, maximumPercentDeviation);
if (bid > tick.Value)
{
bid = tick.Value - (bid - tick.Value);
}
var ask = _random.NextPrice(Symbol.SecurityType, Symbol.ID.Market, tick.Value, maximumPercentDeviation);
if (ask < tick.Value)
{
ask = tick.Value + (tick.Value - ask);
}
tick.BidPrice = bid;
tick.BidSize = _random.NextInt(1, 1500);
tick.AskPrice = ask;
tick.AskSize = _random.NextInt(1, 1500);
return tick;
default:
throw new ArgumentOutOfRangeException(nameof(tickType), tickType, null);
}
}
/// <summary>
/// Generates a random <see cref="Tick"/> that is at most the specified <paramref name="maximumPercentDeviation"/> away from the
/// <paramref name="previousValue"/> and is of the Open Interest
/// </summary>
/// <param name="dateTime">The time of the generated tick</param>
/// <param name="previousValue">The previous price, used as a reference for generating
/// new random prices for the next time step</param>
/// <param name="maximumPercentDeviation">The maximum percentage to deviate from the
/// <paramref name="previousValue"/>, for example, 1 would indicate a maximum of 1% deviation from the
/// <paramref name="previousValue"/>. For a previous price of 100, this would yield a price between 99 and 101 inclusive</param>
/// <returns>A random <see cref="Tick"/> value that is within the specified <paramref name="maximumPercentDeviation"/>
/// from the <paramref name="previousValue"/></returns>
public Tick NextOpenInterest(DateTime dateTime, decimal previousValue, decimal maximumPercentDeviation)
{
var next = (long)_random.NextPrice(Symbol.SecurityType, Symbol.ID.Market, previousValue, maximumPercentDeviation);
return new OpenInterest
{
Time = dateTime,
Symbol = Symbol,
TickType = TickType.OpenInterest,
Value = next,
Quantity = next
};
}
/// <summary>
/// Generates a random <see cref="DateTime"/> suitable for use as a tick's emit time.
/// If the density provided is <see cref="DataDensity.Dense"/>, then at least one tick will be generated per <paramref name="resolution"/> step.
/// If the density provided is <see cref="DataDensity.Sparse"/>, then at least one tick will be generated every 5 <paramref name="resolution"/> steps.
/// if the density provided is <see cref="DataDensity.VerySparse"/>, then at least one tick will be generated every 50 <paramref name="resolution"/> steps.
/// Times returned are guaranteed to be within market hours for the specified Symbol
/// </summary>
/// <param name="previous">The previous tick time</param>
/// <param name="resolution">The requested resolution of data</param>
/// <param name="density">The requested data density</param>
/// <returns>A new <see cref="DateTime"/> that is after <paramref name="previous"/> according to the specified <paramref name="resolution"/>
/// and <paramref name="density"/> specified</returns>
public virtual DateTime NextTickTime(DateTime previous, Resolution resolution, DataDensity density)
{
var increment = resolution.ToTimeSpan();
if (increment == TimeSpan.Zero)
{
increment = TimeSpan.FromMilliseconds(500);
}
double steps;
switch (density)
{
case DataDensity.Dense:
steps = 0.5 * _random.NextDouble();
break;
case DataDensity.Sparse:
steps = 5 * _random.NextDouble();
break;
case DataDensity.VerySparse:
steps = 50 * _random.NextDouble();
break;
default:
throw new ArgumentOutOfRangeException(nameof(density), density, null);
}
var delta = TimeSpan.FromTicks((long)(steps * increment.Ticks));
var tickTime = previous.Add(delta);
if (tickTime == previous)
{
tickTime = tickTime.Add(increment);
}
var barStart = tickTime.Subtract(increment);
var marketHours = MarketHoursDatabase.GetExchangeHours(Symbol.ID.Market, Symbol, Symbol.SecurityType);
if (!marketHours.IsDateOpen(tickTime) || !marketHours.IsOpen(barStart, tickTime, false))
{
// we ended up outside of market hours, emit a new tick at market open
var nextMarketOpen = marketHours.GetNextMarketOpen(tickTime, false);
if (resolution == Resolution.Tick)
{
resolution = Resolution.Second;
}
// emit a new tick somewhere in the next trading day at a step higher resolution to guarantee a hit
return NextTickTime(nextMarketOpen, resolution - 1, density);
}
return tickTime;
}
private static decimal GetMaximumDeviation(Resolution resolution)
{
var incr = ((int)resolution) + 0.15m;
var deviation = incr * incr * 0.1m;
return deviation;
}
}
}
@@ -0,0 +1,13 @@
namespace QuantConnect.ToolBox.RandomDataGenerator
{
/// <summary>
/// Exception thrown when multiple attempts to generate a valid random value end in failure
/// </summary>
public class TooManyFailedAttemptsException : RandomValueGeneratorException
{
public TooManyFailedAttemptsException(string method, int attempts)
: base($"Failed to generate a valid value for '{method}' after {attempts} attempts.")
{
}
}
}