/* * 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 System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.IO.Compression; using System.Linq; namespace QuantConnect.ToolBox { /// /// Generates a factor file from a list of splits and dividends for a specified equity /// public class FactorFileGenerator { /// /// Data for this equity at daily resolution /// private readonly List _dailyDataForEquity; /// /// The last date in the _dailyEquityData /// private readonly DateTime _lastDateFromEquityData; /// /// The symbol for which the factor file is being generated /// public Symbol Symbol { get; set; } /// /// Constructor for the FactorFileGenerator /// /// The equity for which the factor file respresents /// The path to the daily data for the specified equity public FactorFileGenerator(Symbol symbol, string pathForDailyEquityData) { Symbol = symbol; _dailyDataForEquity = ReadDailyEquityData(pathForDailyEquityData); _lastDateFromEquityData = _dailyDataForEquity.Last().Time; } /// /// Create FactorFile instance /// /// List of Dividends and Splits /// instance public CorporateFactorProvider CreateFactorFile(List dividendSplitList) { var orderedDividendSplitQueue = new Queue( CombineIntraDayDividendSplits(dividendSplitList) .OrderByDescending(x => x.Time)); var factorFileRows = new List { // First Factor Row is set far into the future and by definition has 1 for both price and split factors new CorporateFactorRow( Time.EndOfTime, priceFactor: 1, splitFactor: 1 ) }; return RecursivlyGenerateFactorFile(orderedDividendSplitQueue, factorFileRows); } /// /// If dividend and split occur on the same day, /// combine them into IntraDayDividendSplit object /// /// List of split and dividends /// A list of splits, dividends with intraday split and dividends combined into private static List CombineIntraDayDividendSplits(List splitDividendList) { var splitDividendCollection = new Collection(splitDividendList); var dateKeysLookup = splitDividendCollection.GroupBy(x => x.Time) .OrderByDescending(x => x.Key) .Select(group => group) .ToList(); var baseDataList = new List(); foreach (var kvpLookup in dateKeysLookup) { if (kvpLookup.Count() > 1) { // Intraday dividend split found var dividend = kvpLookup.First(x => x.GetType() == typeof(Dividend)) as Dividend; var split = kvpLookup.First(x => x.GetType() == typeof(Split)) as Split; baseDataList.Add(new IntraDayDividendSplit(split, dividend)); } else { baseDataList.Add(kvpLookup.First()); } } return baseDataList; } /// /// Recursively generate a /// /// Queue of dividends and splits ordered by date /// The list of factor file rows /// instance private CorporateFactorProvider RecursivlyGenerateFactorFile(Queue orderedDividendSplits, List factorFileRows) { // If there is no more dividends or splits, return if (!orderedDividendSplits.Any()) { factorFileRows.Add(CreateLastFactorFileRow(factorFileRows, _dailyDataForEquity.Last().Close)); return new CorporateFactorProvider(Symbol.ID.Symbol, factorFileRows); } var nextEvent = orderedDividendSplits.Dequeue(); // If there is no more daily equity data to use, return if (_lastDateFromEquityData > nextEvent.Time) { decimal initialReferencePrice = 1; factorFileRows.Add(CreateLastFactorFileRow(factorFileRows, initialReferencePrice)); return new CorporateFactorProvider(Symbol.ID.Symbol, factorFileRows); } var nextFactorFileRow = CalculateNextFactorFileRow(factorFileRows, nextEvent); if (nextFactorFileRow != null) factorFileRows.Add(nextFactorFileRow); return RecursivlyGenerateFactorFile(orderedDividendSplits, factorFileRows); } /// /// Create the last FileFactorRow. /// Represents the earliest date that the daily equity data contains. /// /// The list of factor file rows /// private CorporateFactorRow CreateLastFactorFileRow(List factorFileRows, decimal referencePrice) { return new CorporateFactorRow( _dailyDataForEquity.Last().Time.Date, factorFileRows.Last().PriceFactor, factorFileRows.Last().SplitFactor, referencePrice ); } /// /// Calculates the next /// /// The current list of factorFileRows /// The next dividend, split or intradayDividendSplit /// A single factor file row private CorporateFactorRow CalculateNextFactorFileRow(List factorFileRows, BaseData nextEvent) { CorporateFactorRow nextCorporateFactorRow; var t = nextEvent.GetType(); switch (t.Name) { case "Dividend": nextCorporateFactorRow = CalculateNextDividendFactor(nextEvent, factorFileRows.Last()); break; case "Split": nextCorporateFactorRow = CalculateNextSplitFactor(nextEvent, factorFileRows.Last()); break; case "IntraDayDividendSplit": nextCorporateFactorRow = CalculateIntradayDividendSplit((IntraDayDividendSplit)nextEvent, factorFileRows.Last()); break; default: throw new ArgumentException("Unhandled BaseData type for FactorFileGenerator."); } return nextCorporateFactorRow; } /// /// Generates the that represents a intraday dividend split. /// Applies the dividend first. /// /// instance that holds the intraday dividend and split information /// The last generated recursivly /// that represents an intraday dividend and split private CorporateFactorRow CalculateIntradayDividendSplit(IntraDayDividendSplit intraDayDividendSplit, CorporateFactorRow last) { var row = CalculateNextDividendFactor(intraDayDividendSplit.Dividend, last); return CalculateNextSplitFactor(intraDayDividendSplit.Split, row); } /// /// Calculates the price factor of a /// /// The next dividend /// The previous generated /// that represents the dividend event private CorporateFactorRow CalculateNextDividendFactor(BaseData dividend, CorporateFactorRow previousCorporateFactorRow) { var eventDayData = GetDailyDataForDate(dividend.Time); // If you don't have the equity data nothing can be calculated if (eventDayData == null) { return null; } TradeBar previousClosingPrice = FindPreviousTradableDayClosingPrice(eventDayData.Time); // adjust the dividend for both price and split factors (!) var priceFactor = previousCorporateFactorRow.PriceFactor * (1 - dividend.Value * previousCorporateFactorRow.SplitFactor / previousClosingPrice.Close); return new CorporateFactorRow( previousClosingPrice.Time, priceFactor.RoundToSignificantDigits(7), previousCorporateFactorRow.SplitFactor, previousClosingPrice.Close ); } /// /// Calculates the split factor of a /// /// The next /// The previous generated /// that represents the split event private CorporateFactorRow CalculateNextSplitFactor(BaseData split, CorporateFactorRow previousCorporateFactorRow) { var eventDayData = GetDailyDataForDate(split.Time); // If you don't have the equity data nothing can be done if (eventDayData == null) { return null; } TradeBar previousClosingPrice = FindPreviousTradableDayClosingPrice(eventDayData.Time); return new CorporateFactorRow( previousClosingPrice.Time, previousCorporateFactorRow.PriceFactor, (previousCorporateFactorRow.SplitFactor / split.Value).RoundToSignificantDigits(6), previousClosingPrice.Close ); } /// /// Gets the data for a specified date /// /// The current specified date /// representing that date private TradeBar GetDailyDataForDate(DateTime date) { return _dailyDataForEquity.FirstOrDefault(x => x.Time.Date == date.Date); } /// /// Gets the data for the previous tradable day /// /// The current specified date /// The last tradeble days data private TradeBar FindPreviousTradableDayClosingPrice(DateTime date) { TradeBar previousDayData = null; var lastDateforData = _dailyDataForEquity.Last(); while (previousDayData == null && date > lastDateforData.EndTime) { previousDayData = _dailyDataForEquity.FirstOrDefault(x => x.Time == date.AddDays(-1)); date = date.AddDays(-1); } return previousDayData; } /// /// Read the daily equity date from file /// /// Path the the daily data /// A list of read from file private static List ReadDailyEquityData(string pathForDailyEquityData) { var dataReader = new LeanDataReader(pathForDailyEquityData); var bars = dataReader.Parse(); return bars.OrderByDescending(x => x.Time) .Select(x => (TradeBar)x) .ToList(); } /// /// Pairs split and dividend data into one type /// private class IntraDayDividendSplit : BaseData { public Split Split { get; } public Dividend Dividend { get; } public IntraDayDividendSplit(Split split, Dividend dividend) { if (split == null) { throw new ArgumentNullException(nameof(split)); } if (dividend == null) { throw new ArgumentNullException(nameof(dividend)); } Split = split; Dividend = dividend; Time = Split.Time; } } } }