/*
* 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
{
///
/// Provides an implementation of that uses
/// to generate random values
///
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);
///
/// Generates a random suitable as a price. This should observe minimum price
/// variations if available in , and if not, truncating to 2
/// decimal places.
///
/// Throw when the or
/// is less than or equal to zero.
/// The security type the price is being generated for
/// The market of the security the price is being generated for
/// The reference price used as the mean of random price generation
/// The maximum percent deviation. This value is in percent space,
/// so a value of 1m is equal to 1%.
/// A new decimal suitable for usage as price within the specified deviation from the reference price
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;
}
}
}
}
}