/* * 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 QuantConnect.Util; using System.Threading.Tasks; using QuantConnect.Interfaces; using System.Collections.Generic; using QuantConnect.Data.Auxiliary; using QuantConnect.Securities; using QuantConnect.Data.Market; namespace QuantConnect.Data { /// /// Estimated annualized continuous dividend yield at given date /// public class DividendYieldProvider : IDividendYieldModel { private static MarketHoursDatabase _marketHoursDatabase = MarketHoursDatabase.FromDataFolder(); /// /// The default symbol to use as a dividend yield provider /// /// This is useful for index and future options which do not have an underlying that yields dividends. /// Defaults to SPY public static Symbol DefaultSymbol { get; set; } = Symbol.Create("SPY", SecurityType.Equity, QuantConnect.Market.USA); /// /// The dividends by symbol /// protected static Dictionary> _corporateEventsCache; /// /// Task to clear the cache /// protected static Task _cacheClearTask; private static readonly object _lock = new(); private readonly Symbol _symbol; private readonly SecurityExchangeHours _exchangeHours; /// /// Default no dividend payout /// public static readonly decimal DefaultDividendYieldRate = 0.0m; /// /// The cached refresh period for the dividend yield rate /// /// Exposed for testing protected virtual TimeSpan CacheRefreshPeriod { get { var dueTime = Time.GetNextLiveAuxiliaryDataDueTime(); if (dueTime > TimeSpan.FromMinutes(10)) { // Clear the cache before the auxiliary due time to avoid race conditions with consumers return dueTime - TimeSpan.FromMinutes(10); } return dueTime; } } /// /// Creates a new instance using the default symbol /// public DividendYieldProvider() : this(DefaultSymbol) { } /// /// Instantiates a with the specified Symbol /// public DividendYieldProvider(Symbol symbol) { _symbol = symbol; _exchangeHours = _marketHoursDatabase.GetExchangeHours(symbol.ID.Market, symbol, symbol.ID.SecurityType); if (_cacheClearTask == null) { lock (_lock) { // only the first triggers the expiration task check if (_cacheClearTask == null) { StartExpirationTask(CacheRefreshPeriod); } } } } /// /// Creates a new instance for the given option symbol /// public static IDividendYieldModel CreateForOption(Symbol optionSymbol) { if (optionSymbol.SecurityType == SecurityType.Option) { return new DividendYieldProvider(optionSymbol.Underlying); } if (optionSymbol.SecurityType == SecurityType.IndexOption) { return optionSymbol.Value switch { "SPX" => new DividendYieldProvider(Symbol.Create("SPY", SecurityType.Equity, QuantConnect.Market.USA)), "NDX" => new DividendYieldProvider(Symbol.Create("QQQ", SecurityType.Equity, QuantConnect.Market.USA)), "VIX" => new ConstantDividendYieldModel(0), _ => new DividendYieldProvider() }; } return new ConstantDividendYieldModel(0); } /// /// Helper method that will clear any cached dividend rate in a daily basis, this is useful for live trading /// private static void StartExpirationTask(TimeSpan cacheRefreshPeriod) { lock (_lock) { // we clear the dividend yield rate cache so they are reloaded _corporateEventsCache = new(); } _cacheClearTask = Task.Delay(cacheRefreshPeriod).ContinueWith(_ => StartExpirationTask(cacheRefreshPeriod)); } /// /// Get dividend yield by a given date of a given symbol. /// It will get the dividend yield at the time of the most recent dividend since no price is provided. /// In order to get more accurate dividend yield, provide the security price at the given date to /// the or methods. /// /// The date /// Dividend yield on the given date of the given symbol public decimal GetDividendYield(DateTime date) { return GetDividendYieldImpl(date, null); } /// /// Gets the dividend yield at the date of the specified data, using the data price as the security price /// /// Price data instance /// Dividend yield on the given date of the given symbol /// Price data must be raw () public decimal GetDividendYield(IBaseData priceData) { if (priceData.Symbol != _symbol) { throw new ArgumentException($"Trying to get {priceData.Symbol} dividend yield using the {_symbol} dividend yield provider."); } return GetDividendYield(priceData.EndTime, priceData.Value); } /// /// Get dividend yield at given date and security price /// /// The date /// The security price at the given date /// Dividend yield on the given date of the given symbol /// Price data must be raw () public decimal GetDividendYield(DateTime date, decimal securityPrice) { return GetDividendYieldImpl(date, securityPrice); } /// /// Get dividend yield at given date and security price. /// /// /// is nullable for backwards compatibility, so is usable. /// If dividend yield is requested at a given date without a price, the dividend yield at the time of the most recent dividend is returned. /// Price data must be raw (). /// private decimal GetDividendYieldImpl(DateTime date, decimal? securityPrice) { List symbolCorporateEvents; lock (_lock) { if (!_corporateEventsCache.TryGetValue(_symbol, out symbolCorporateEvents)) { // load the symbol factor if it is the first encounter symbolCorporateEvents = _corporateEventsCache[_symbol] = LoadCorporateEvents(_symbol); } } if (symbolCorporateEvents == null) { return DefaultDividendYieldRate; } // We need both corporate event types, so we get the most recent one, either dividend or split var mostRecentCorporateEventIndex = symbolCorporateEvents.FindLastIndex(x => x.EndTime <= date.Date); if (mostRecentCorporateEventIndex == -1) { return DefaultDividendYieldRate; } // Now we get the most recent dividend in order to get the end of the trailing twelve months period for the dividend yield var mostRecentCorporateEvent = symbolCorporateEvents[mostRecentCorporateEventIndex]; var mostRecentDividend = mostRecentCorporateEvent as Dividend; if (mostRecentDividend == null) { for (var i = mostRecentCorporateEventIndex - 1; i >= 0; i--) { if (symbolCorporateEvents[i] is Dividend dividend) { mostRecentDividend = dividend; break; } } } // If there is no dividend in the past year, we return the default dividend yield rate if (mostRecentDividend == null) { return DefaultDividendYieldRate; } securityPrice ??= mostRecentDividend.ReferencePrice; if (securityPrice == 0) { throw new ArgumentException("Security price cannot be zero."); } // The dividend yield is the sum of the dividends in the past year (ending in the most recent dividend date, // not on the price quote date) divided by the last close price: // 15 days window from 1y to avoid overestimation from last year value var trailingYearStartDate = mostRecentDividend.EndTime.AddDays(-350); var yearlyDividend = 0m; var currentSplitFactor = 1m; for (var i = mostRecentCorporateEventIndex; i >= 0; i--) { var corporateEvent = symbolCorporateEvents[i]; if (corporateEvent.EndTime < trailingYearStartDate) { break; } if (corporateEvent is Dividend dividend) { yearlyDividend += dividend.Distribution * currentSplitFactor; } else { // Update the split factor to adjust the dividend value per share currentSplitFactor *= ((Split)corporateEvent).SplitFactor; } } return yearlyDividend / securityPrice.Value; } /// /// Generate the corporate events from the corporate factor file for the specified symbol /// /// Exposed for testing protected virtual List LoadCorporateEvents(Symbol symbol) { var factorFileProvider = Composer.Instance.GetPart(); var corporateFactors = factorFileProvider .Get(symbol) .Select(factorRow => factorRow as CorporateFactorRow) .Where(corporateFactor => corporateFactor != null); var symbolCorporateEvents = FromCorporateFactorRows(corporateFactors, symbol).ToList(); if (symbolCorporateEvents.Count == 0) { return null; } return symbolCorporateEvents; } /// /// Generates the splits and dividends from the corporate factor rows /// private IEnumerable FromCorporateFactorRows(IEnumerable corporateFactors, Symbol symbol) { var dividends = new List(); // Get all dividends from the corporate actions var rows = corporateFactors.OrderBy(corporateFactor => corporateFactor.Date).ToArray(); for (var i = 0; i < rows.Length - 1; i++) { var row = rows[i]; var nextRow = rows[i + 1]; if (row.PriceFactor != nextRow.PriceFactor) { yield return row.GetDividend(nextRow, symbol, _exchangeHours, decimalPlaces: 3); } else { yield return row.GetSplit(nextRow, symbol, _exchangeHours); } } } } }