/*
* 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
{
///
/// Generates random splits, random dividends, and map file
///
public class DividendSplitMapGenerator
{
private const double _minimumFinalSplitFactorAllowed = 0.001;
///
/// The final factor to adjust all prices with in order to maintain price continuity.
///
///
/// Set default equal to 1 so that we can use it even in the event of no splits
///
public decimal FinalSplitFactor { get; set; } = 1m;
///
/// Stores instances
///
public List MapRows { get; set; } = new();
///
/// Stores instances
///
public List DividendsSplits { get; set; } = new List();
///
/// Current Symbol value. Can be renamed
///
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;
}
///
/// Generates the splits, dividends, and maps.
/// Writes necessary output to public variables
///
///
public void GenerateSplitsDividends(IEnumerable 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();
var dividendDates = new List();
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();
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();
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;
}
}
}
///
/// Gets a lower bound that guarantees the FinalSplitFactor, in all the possible
/// cases, will never be smaller than the _minimumFinalSplitFactorAllowed (0.001)
///
/// The lower bound for the previous split factor is based on
/// the number of months between the start and end date from ticksHistory
/// A valid lower bound that guarantees the FinalSplitFactor is always higher
/// than the _minimumFinalSplitFactorAllowed
public static decimal GetLowerBoundForPreviousSplitFactor(int months)
{
return (decimal)(Math.Pow(_minimumFinalSplitFactorAllowed, 1 / (double)(2 * months)));
}
///
/// Gets a new valid previousSplitFactor that is still bounded by the given upper and lower
/// bounds
///
/// Random number generator
/// Minimum allowed value to obtain
/// Maximum allowed value to obtain
/// A new valid previousSplitFactor that is still bounded by the given upper and lower
/// bounds
public static decimal GetNextPreviousSplitFactor(Random random, decimal lowerBound, decimal upperBound)
{
return ((decimal)random.NextDouble()) * (upperBound - lowerBound) + lowerBound;
}
}
}