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