/*
* 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;
}
}
}
}