Lean  $LEAN_TAG$
DividendYieldProvider.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Linq;
18 using QuantConnect.Util;
19 using QuantConnect.Logging;
20 using System.Threading.Tasks;
22 using System.Collections.Generic;
24 
25 namespace QuantConnect.Data
26 {
27  /// <summary>
28  /// Estimated annualized continuous dividend yield at given date
29  /// </summary>
31  {
32  protected static Dictionary<Symbol, Dictionary<DateTime, decimal>> _dividendYieldRateProvider;
33  protected static Task _cacheClearTask;
34  private static readonly object _lock = new();
35 
36  private DateTime _firstDividendYieldDate = Time.Start;
37  private decimal _lastDividendYield = -1;
38  private readonly Symbol _symbol;
39 
40  /// <summary>
41  /// Default no dividend payout
42  /// </summary>
43  public static readonly decimal DefaultDividendYieldRate = 0.0m;
44 
45  /// <summary>
46  /// The cached refresh period for the dividend yield rate
47  /// </summary>
48  /// <remarks>Exposed for testing</remarks>
49  protected virtual TimeSpan CacheRefreshPeriod
50  {
51  get
52  {
53  var dueTime = Time.GetNextLiveAuxiliaryDataDueTime();
54  if (dueTime > TimeSpan.FromMinutes(10))
55  {
56  // Clear the cache before the auxiliary due time to avoid race conditions with consumers
57  return dueTime - TimeSpan.FromMinutes(10);
58  }
59  return dueTime;
60  }
61  }
62 
63  /// <summary>
64  /// Instantiates a <see cref="DividendYieldProvider"/> with the specified Symbol
65  /// </summary>
67  {
68  _symbol = symbol;
69 
70  if (_cacheClearTask == null)
71  {
72  lock (_lock)
73  {
74  // only the first triggers the expiration task check
75  if (_cacheClearTask == null)
76  {
77  StartExpirationTask(CacheRefreshPeriod);
78  }
79  }
80  }
81  }
82 
83  /// <summary>
84  /// Helper method that will clear any cached dividend rate in a daily basis, this is useful for live trading
85  /// </summary>
86  private static void StartExpirationTask(TimeSpan cacheRefreshPeriod)
87  {
88  lock (_lock)
89  {
90  // we clear the dividend yield rate cache so they are reloaded
91  _dividendYieldRateProvider = new();
92  }
93  _cacheClearTask = Task.Delay(cacheRefreshPeriod).ContinueWith(_ => StartExpirationTask(cacheRefreshPeriod));
94  }
95 
96  /// <summary>
97  /// Get dividend yield by a given date of a given symbol
98  /// </summary>
99  /// <param name="date">The date</param>
100  /// <returns>Dividend yield on the given date of the given symbol</returns>
101  public decimal GetDividendYield(DateTime date)
102  {
103  Dictionary<DateTime, decimal> symbolDividend;
104  lock (_lock)
105  {
106  if (!_dividendYieldRateProvider.TryGetValue(_symbol, out symbolDividend))
107  {
108  // load the symbol factor if it is the first encounter
109  symbolDividend = _dividendYieldRateProvider[_symbol] = LoadDividendYieldProvider(_symbol);
110  }
111  }
112 
113  if (symbolDividend == null)
114  {
116  }
117 
118  if (!symbolDividend.TryGetValue(date.Date, out var dividendYield))
119  {
120  if (_lastDividendYield == -1)
121  {
122  _firstDividendYieldDate = symbolDividend.OrderBy(x => x.Key).First().Key;
123  _lastDividendYield = symbolDividend.OrderBy(x => x.Key).Last().Value;
124  }
125  return date < _firstDividendYieldDate
127  : _lastDividendYield;
128  }
129 
130  return dividendYield;
131  }
132 
133  /// <summary>
134  /// Generate the daily historical dividend yield
135  /// </summary>
136  /// <remarks>Exposed for testing</remarks>
137  protected virtual Dictionary<DateTime, decimal> LoadDividendYieldProvider(Symbol symbol)
138  {
139  var factorFileProvider = Composer.Instance.GetPart<IFactorFileProvider>();
140  var corporateFactors = factorFileProvider
141  .Get(symbol)
142  .Select(factorRow => factorRow as CorporateFactorRow)
143  .Where(corporateFactor => corporateFactor != null);
144 
145  var symbolDividends = FromCorporateFactorRow(corporateFactors, symbol);
146  if (symbolDividends.Count == 0)
147  {
148  return null;
149  }
150 
151  _firstDividendYieldDate = symbolDividends.Keys.Min();
152  var lastDate = symbolDividends.Keys.Where(x => x != Time.EndOfTime).Max();
153 
154  // Sparse the discrete data points into continuous data for every day
155  _lastDividendYield = DefaultDividendYieldRate;
156  for (var date = _firstDividendYieldDate; date <= lastDate; date = date.AddDays(1))
157  {
158  if (!symbolDividends.TryGetValue(date, out var currentRate))
159  {
160  symbolDividends[date] = _lastDividendYield;
161  continue;
162  }
163  _lastDividendYield = currentRate;
164  }
165 
166  return symbolDividends;
167  }
168 
169  /// <summary>
170  /// Returns a dictionary of historical dividend yield from collection of corporate factor rows
171  /// </summary>
172  /// <param name="corporateFactors">The corporate factor rows containing factor data</param>
173  /// <param name="symbol">The target symbol</param>
174  /// <returns>Dictionary of historical annualized continuous dividend yield data</returns>
175  public static Dictionary<DateTime, decimal> FromCorporateFactorRow(IEnumerable<CorporateFactorRow> corporateFactors, Symbol symbol)
176  {
177  var dividendYieldProvider = new Dictionary<DateTime, decimal>();
178 
179  // calculate the dividend rate from each payout
180  var subsequentRate = 0m;
181  foreach (var row in corporateFactors.Where(x => x.Date != Time.EndOfTime).OrderByDescending(corporateFactor => corporateFactor.Date))
182  {
183  var dividendYield = 1 / row.PriceFactor - 1 - subsequentRate;
184  dividendYieldProvider[row.Date] = dividendYield;
185  subsequentRate = dividendYield;
186  }
187 
188  // cumulative sum by year, since we'll use yearly payouts for estimation
189  var yearlyDividendYieldProvider = new Dictionary<DateTime, decimal>();
190  foreach (var date in dividendYieldProvider.Keys.OrderBy(x => x))
191  {
192  // 15 days window from 1y to avoid overestimation from last year value
193  var yearlyDividend = dividendYieldProvider.Where(kvp => kvp.Key <= date && kvp.Key > date.AddDays(-350)).Sum(kvp => kvp.Value);
194  // discrete to continuous: LN(1 + i)
195  yearlyDividendYieldProvider[date] = Convert.ToDecimal(Math.Log(1d + (double)yearlyDividend));
196  }
197 
198  if (yearlyDividendYieldProvider.Count == 0)
199  {
200  Log.Error($"DividendYieldProvider.FromCsvFile({symbol}): no dividend were loaded");
201  }
202 
203  return yearlyDividendYieldProvider;
204  }
205  }
206 }