/*
* 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.Linq;
using System.Globalization;
using QuantConnect.Securities;
using QuantConnect.Data.Market;
using System.Collections.Generic;
using static QuantConnect.StringExtensions;
namespace QuantConnect.Data.Auxiliary
{
///
/// Defines a single row in a factor_factor file. This is a csv file ordered as {date, price factor, split factor, reference price}
///
public class CorporateFactorRow : IFactorRow
{
private decimal _splitFactor;
private decimal _priceFactor;
///
/// Gets the date associated with this data
///
public DateTime Date { get; private set; }
///
/// Gets the price factor associated with this data
///
public decimal PriceFactor
{
get
{
return _priceFactor;
}
set
{
_priceFactor = value;
UpdatePriceScaleFactor();
}
}
///
/// Gets the split factor associated with the date
///
public decimal SplitFactor
{
get
{
return _splitFactor;
}
set
{
_splitFactor = value;
UpdatePriceScaleFactor();
}
}
///
/// Gets the combined factor used to create adjusted prices from raw prices
///
public decimal PriceScaleFactor { get; private set; }
///
/// Gets the raw closing value from the trading date before the updated factor takes effect
///
public decimal ReferencePrice { get; private set; }
///
/// Initializes a new instance of the class
///
public CorporateFactorRow(DateTime date, decimal priceFactor, decimal splitFactor, decimal referencePrice = 0)
{
Date = date;
ReferencePrice = referencePrice;
PriceFactor = priceFactor;
SplitFactor = splitFactor;
}
///
/// Parses the lines as factor files rows while properly handling inf entries
///
/// The lines from the factor file to be parsed
/// The minimum date from the factor file
/// An enumerable of factor file rows
public static List Parse(IEnumerable lines, out DateTime? factorFileMinimumDate)
{
factorFileMinimumDate = null;
var rows = new List();
// parse factor file lines
foreach (var line in lines)
{
// Exponential notation is treated as inf is because of the loss of precision. In
// all cases, the significant part has fewer decimals than the needed for a correct
// representation, E.g., 1.6e+6 when the correct factor is 1562500.
if (line.Contains("inf") || line.Contains("e+"))
{
continue;
}
var row = Parse(line);
// ignore zero factor rows
if (row.PriceScaleFactor > 0)
{
rows.Add(row);
}
}
if (rows.Count > 0)
{
factorFileMinimumDate = rows.Min(ffr => ffr.Date).AddDays(-1);
}
return rows;
}
///
/// Applies the dividend to this factor file row.
/// This dividend date must be on or before the factor
/// file row date
///
/// The dividend to apply with reference price and distribution specified
/// Exchange hours used for resolving the previous trading day
/// A new factor file row that applies the dividend to this row's factors
public CorporateFactorRow Apply(Dividend dividend, SecurityExchangeHours exchangeHours)
{
if (dividend.ReferencePrice == 0m)
{
throw new ArgumentException("Unable to apply dividend with reference price of zero.");
}
var previousTradingDay = exchangeHours.GetPreviousTradingDay(dividend.Time);
// this instance must be chronologically at or in front of the dividend
// this is because the factors are defined working from current to past
if (Date < previousTradingDay)
{
throw new ArgumentException(Invariant(
$"Factor file row date '{Date:yyy-MM-dd}' is before dividend previous trading date '{previousTradingDay.Date:yyyy-MM-dd}'."
));
}
// pfi - new price factor pf(i+1) - this price factor D - distribution C - previous close
// pfi = pf(i+1) * (C-D)/C
var priceFactor = PriceFactor * (dividend.ReferencePrice - dividend.Distribution) / dividend.ReferencePrice;
return new CorporateFactorRow(
previousTradingDay,
priceFactor,
SplitFactor,
dividend.ReferencePrice
);
}
///
/// Applies the split to this factor file row.
/// This split date must be on or before the factor
/// file row date
///
/// The split to apply with reference price and split factor specified
/// Exchange hours used for resolving the previous trading day
/// A new factor file row that applies the split to this row's factors
public CorporateFactorRow Apply(Split split, SecurityExchangeHours exchangeHours)
{
if (split.Type == SplitType.Warning)
{
throw new ArgumentException("Unable to apply split with type warning. Only actual splits may be applied");
}
if (split.ReferencePrice == 0m)
{
throw new ArgumentException("Unable to apply split with reference price of zero.");
}
var previousTradingDay = exchangeHours.GetPreviousTradingDay(split.Time);
// this instance must be chronologically at or in front of the split
// this is because the factors are defined working from current to past
if (Date < previousTradingDay)
{
throw new ArgumentException(Invariant(
$"Factor file row date '{Date:yyy-MM-dd}' is before split date '{split.Time.Date:yyyy-MM-dd}'."
));
}
return new CorporateFactorRow(
previousTradingDay,
PriceFactor,
SplitFactor * split.SplitFactor,
split.ReferencePrice
);
}
///
/// Creates a new dividend from this factor file row and the one chronologically in front of it
/// This dividend may have a distribution of zero if this row doesn't represent a dividend
///
/// The next factor file row in time
/// The symbol to use for the dividend
/// Exchange hours used for resolving the previous trading day
/// The number of decimal places to round the dividend's distribution to, defaulting to 2
/// A new dividend instance
public Dividend GetDividend(CorporateFactorRow nextCorporateFactorRow, Symbol symbol, SecurityExchangeHours exchangeHours, int decimalPlaces=2)
{
if (nextCorporateFactorRow.PriceFactor == 0m)
{
throw new InvalidOperationException(Invariant(
$"Unable to resolve dividend for '{symbol.ID}' at {Date:yyyy-MM-dd}. Price factor is zero."
));
}
// find previous trading day
var previousTradingDay = exchangeHours.GetNextTradingDay(Date);
return Dividend.Create(
symbol,
previousTradingDay,
ReferencePrice,
PriceFactor / nextCorporateFactorRow.PriceFactor,
decimalPlaces
);
}
///
/// Creates a new split from this factor file row and the one chronologically in front of it
/// This split may have a split factor of one if this row doesn't represent a split
///
/// The next factor file row in time
/// The symbol to use for the split
/// Exchange hours used for resolving the previous trading day
/// A new split instance
public Split GetSplit(CorporateFactorRow nextCorporateFactorRow, Symbol symbol, SecurityExchangeHours exchangeHours)
{
if (nextCorporateFactorRow.SplitFactor == 0m)
{
throw new InvalidOperationException(Invariant(
$"Unable to resolve split for '{symbol.ID}' at {Date:yyyy-MM-dd}. Split factor is zero."
));
}
// find previous trading day
var previousTradingDay = exchangeHours.GetNextTradingDay(Date);
return new Split(
symbol,
previousTradingDay,
ReferencePrice,
SplitFactor / nextCorporateFactorRow.SplitFactor,
SplitType.SplitOccurred
);
}
///
/// Parses the specified line as a factor file row
///
private static CorporateFactorRow Parse(string line)
{
var csv = line.Split(',');
return new CorporateFactorRow(
QuantConnect.Parse.DateTimeExact(csv[0], DateFormat.EightCharacter, DateTimeStyles.None),
QuantConnect.Parse.Decimal(csv[1]),
QuantConnect.Parse.Decimal(csv[2]),
csv.Length > 3 ? QuantConnect.Parse.Decimal(csv[3]) : 0m
);
}
///
/// Writes factor file row into it's file format
///
/// CSV formatted
public string GetFileFormat(string source = null)
{
source = source == null ? "" : $",{source}";
return $"{Date.ToStringInvariant(DateFormat.EightCharacter)}," +
Invariant($"{Math.Round(PriceFactor, 7)},") +
Invariant($"{Math.Round(SplitFactor, 8)},") +
Invariant($"{Math.Round(ReferencePrice, 4).Normalize()}") +
$"{source}";
}
///
/// Returns a string that represents the current object.
///
///
/// A string that represents the current object.
///
/// 2
public override string ToString()
{
return Invariant($"{Date:yyyy-MM-dd}: {PriceScaleFactor:0.0000} {SplitFactor:0.0000}");
}
///
/// For performance we update when underlying
/// values are updated to avoid decimal multiplication on each get operation.
///
private void UpdatePriceScaleFactor()
{
PriceScaleFactor = _priceFactor * _splitFactor;
}
}
}