Lean  $LEAN_TAG$
MarketHoursDatabase.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.Collections.Generic;
18 using System.IO;
19 using System.Linq;
20 using Newtonsoft.Json;
21 using NodaTime;
22 using QuantConnect.Data;
23 using QuantConnect.Logging;
25 using QuantConnect.Util;
26 
28 {
29  /// <summary>
30  /// Provides access to exchange hours and raw data times zones in various markets
31  /// </summary>
32  [JsonConverter(typeof(MarketHoursDatabaseJsonConverter))]
33  public class MarketHoursDatabase
34  {
35  private static MarketHoursDatabase _dataFolderMarketHoursDatabase;
36  private static MarketHoursDatabase _alwaysOpenMarketHoursDatabase;
37  private static readonly object DataFolderMarketHoursDatabaseLock = new object();
38 
39  private Dictionary<SecurityDatabaseKey, Entry> _entries;
40  private readonly Dictionary<SecurityDatabaseKey, Entry> _customEntries = new();
41 
42  /// <summary>
43  /// Gets all the exchange hours held by this provider
44  /// </summary>
45  public List<KeyValuePair<SecurityDatabaseKey,Entry>> ExchangeHoursListing => _entries.ToList();
46 
47  /// <summary>
48  /// Gets a <see cref="MarketHoursDatabase"/> that always returns <see cref="SecurityExchangeHours.AlwaysOpen"/>
49  /// </summary>
50  public static MarketHoursDatabase AlwaysOpen
51  {
52  get
53  {
54  if (_alwaysOpenMarketHoursDatabase == null)
55  {
56  _alwaysOpenMarketHoursDatabase = new AlwaysOpenMarketHoursDatabaseImpl();
57  }
58 
59  return _alwaysOpenMarketHoursDatabase;
60  }
61  }
62 
63  /// <summary>
64  /// Initializes a new instance of the <see cref="MarketHoursDatabase"/> class
65  /// </summary>
66  /// <param name="exchangeHours">The full listing of exchange hours by key</param>
67  public MarketHoursDatabase(Dictionary<SecurityDatabaseKey, Entry> exchangeHours)
68  {
69  _entries = exchangeHours;
70  }
71 
72  /// <summary>
73  /// Convenience method for retrieving exchange hours from market hours database using a subscription config
74  /// </summary>
75  /// <param name="configuration">The subscription data config to get exchange hours for</param>
76  /// <returns>The configure exchange hours for the specified configuration</returns>
78  {
79  return GetExchangeHours(configuration.Market, configuration.Symbol, configuration.SecurityType);
80  }
81 
82  /// <summary>
83  /// Convenience method for retrieving exchange hours from market hours database using a subscription config
84  /// </summary>
85  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
86  /// <param name="symbol">The particular symbol being traded</param>
87  /// <param name="securityType">The security type of the symbol</param>
88  /// <returns>The exchange hours for the specified security</returns>
89  public SecurityExchangeHours GetExchangeHours(string market, Symbol symbol, SecurityType securityType)
90  {
91  return GetEntry(market, symbol, securityType).ExchangeHours;
92  }
93 
94  /// <summary>
95  /// Performs a lookup using the specified information and returns the data's time zone if found,
96  /// if an entry is not found, an exception is thrown
97  /// </summary>
98  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
99  /// <param name="symbol">The particular symbol being traded</param>
100  /// <param name="securityType">The security type of the symbol</param>
101  /// <returns>The raw data time zone for the specified security</returns>
102  public DateTimeZone GetDataTimeZone(string market, Symbol symbol, SecurityType securityType)
103  {
104  return GetEntry(market, GetDatabaseSymbolKey(symbol), securityType).DataTimeZone;
105  }
106 
107  /// <summary>
108  /// Resets the market hours database, forcing a reload when reused.
109  /// Called in tests where multiple algorithms are run sequentially,
110  /// and we need to guarantee that every test starts with the same environment.
111  /// </summary>
112  public static void Reset()
113  {
114  lock (DataFolderMarketHoursDatabaseLock)
115  {
116  _dataFolderMarketHoursDatabase = null;
117  }
118  }
119 
120  /// <summary>
121  /// Reload entries dictionary from MHDB file and merge them with previous custom ones
122  /// </summary>
123  public void ReloadEntries()
124  {
125  lock (DataFolderMarketHoursDatabaseLock)
126  {
127  Reset();
128  var fileEntries = FromDataFolder()._entries.Where(x => !_customEntries.ContainsKey(x.Key));
129  var newEntries = fileEntries.Concat(_customEntries).ToDictionary();
130  _entries = newEntries;
131  }
132  }
133 
134  /// <summary>
135  /// Gets the instance of the <see cref="MarketHoursDatabase"/> class produced by reading in the market hours
136  /// data found in /Data/market-hours/
137  /// </summary>
138  /// <returns>A <see cref="MarketHoursDatabase"/> class that represents the data in the market-hours folder</returns>
140  {
141  var result = _dataFolderMarketHoursDatabase;
142  if (result == null)
143  {
144  lock (DataFolderMarketHoursDatabaseLock)
145  {
146  if (_dataFolderMarketHoursDatabase == null)
147  {
148  var path = Path.Combine(Globals.GetDataFolderPath("market-hours"), "market-hours-database.json");
149  _dataFolderMarketHoursDatabase = FromFile(path);
150  }
151  result = _dataFolderMarketHoursDatabase;
152  }
153  }
154  return result;
155  }
156 
157  /// <summary>
158  /// Reads the specified file as a market hours database instance
159  /// </summary>
160  /// <param name="path">The market hours database file path</param>
161  /// <returns>A new instance of the <see cref="MarketHoursDatabase"/> class</returns>
162  public static MarketHoursDatabase FromFile(string path)
163  {
164  return JsonConvert.DeserializeObject<MarketHoursDatabase>(File.ReadAllText(path));
165  }
166 
167  /// <summary>
168  /// Sets the entry for the specified market/symbol/security-type.
169  /// This is intended to be used by custom data and other data sources that don't have explicit
170  /// entries in market-hours-database.csv. At run time, the algorithm can update the market hours
171  /// database via calls to AddData.
172  /// </summary>
173  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
174  /// <param name="symbol">The particular symbol being traded</param>
175  /// <param name="securityType">The security type of the symbol</param>
176  /// <param name="exchangeHours">The exchange hours for the specified symbol</param>
177  /// <param name="dataTimeZone">The time zone of the symbol's raw data. Optional, defaults to the exchange time zone</param>
178  /// <returns>The entry matching the specified market/symbol/security-type</returns>
179  public virtual Entry SetEntry(string market, string symbol, SecurityType securityType, SecurityExchangeHours exchangeHours, DateTimeZone dataTimeZone = null)
180  {
181  dataTimeZone = dataTimeZone ?? exchangeHours.TimeZone;
182  var key = new SecurityDatabaseKey(market, symbol, securityType);
183  var entry = new Entry(dataTimeZone, exchangeHours);
184  _entries[key] = entry;
185  _customEntries[key] = entry;
186  return entry;
187  }
188 
189  /// <summary>
190  /// Convenience method for the common custom data case.
191  /// Sets the entry for the specified symbol using SecurityExchangeHours.AlwaysOpen(timeZone)
192  /// This sets the data time zone equal to the exchange time zone as well.
193  /// </summary>
194  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
195  /// <param name="symbol">The particular symbol being traded</param>
196  /// <param name="securityType">The security type of the symbol</param>
197  /// <param name="timeZone">The time zone of the symbol's exchange and raw data</param>
198  /// <returns>The entry matching the specified market/symbol/security-type</returns>
199  public virtual Entry SetEntryAlwaysOpen(string market, string symbol, SecurityType securityType, DateTimeZone timeZone)
200  {
201  return SetEntry(market, symbol, securityType, SecurityExchangeHours.AlwaysOpen(timeZone));
202  }
203 
204  /// <summary>
205  /// Gets the entry for the specified market/symbol/security-type
206  /// </summary>
207  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
208  /// <param name="symbol">The particular symbol being traded</param>
209  /// <param name="securityType">The security type of the symbol</param>
210  /// <returns>The entry matching the specified market/symbol/security-type</returns>
211  public virtual Entry GetEntry(string market, string symbol, SecurityType securityType)
212  {
213  Entry entry;
214  // Fall back on the Futures MHDB entry if the FOP lookup failed.
215  // Some FOPs have the same symbol properties as their futures counterparts.
216  // So, to save ourselves some space, we can fall back on the existing entries
217  // so that we don't duplicate the information.
218  if (!TryGetEntry(market, symbol, securityType, out entry))
219  {
220  var key = new SecurityDatabaseKey(market, symbol, securityType);
221  Log.Error($"MarketHoursDatabase.GetExchangeHours(): {Messages.MarketHoursDatabase.ExchangeHoursNotFound(key, _entries.Keys)}");
222 
223  if (securityType == SecurityType.Future && market == Market.USA)
224  {
225  var exception = Messages.MarketHoursDatabase.FutureUsaMarketTypeNoLongerSupported;
226  if (SymbolPropertiesDatabase.FromDataFolder().TryGetMarket(symbol, SecurityType.Future, out market))
227  {
228  // let's suggest a market
229  exception += " " + Messages.MarketHoursDatabase.SuggestedMarketBasedOnTicker(market);
230  }
231 
232  throw new ArgumentException(exception);
233  }
234  // there was nothing that really matched exactly
235  throw new ArgumentException(Messages.MarketHoursDatabase.ExchangeHoursNotFound(key));
236  }
237 
238  return entry;
239  }
240 
241  /// <summary>
242  /// Tries to get the entry for the specified market/symbol/security-type
243  /// </summary>
244  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
245  /// <param name="symbol">The particular symbol being traded</param>
246  /// <param name="securityType">The security type of the symbol</param>
247  /// <param name="entry">The entry found if any</param>
248  /// <returns>True if the entry was present, else false</returns>
249  public bool TryGetEntry(string market, Symbol symbol, SecurityType securityType, out Entry entry)
250  {
251  return TryGetEntry(market, GetDatabaseSymbolKey(symbol), securityType, out entry);
252  }
253 
254  /// <summary>
255  /// Tries to get the entry for the specified market/symbol/security-type
256  /// </summary>
257  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
258  /// <param name="symbol">The particular symbol being traded</param>
259  /// <param name="securityType">The security type of the symbol</param>
260  /// <param name="entry">The entry found if any</param>
261  /// <returns>True if the entry was present, else false</returns>
262  public bool TryGetEntry(string market, string symbol, SecurityType securityType, out Entry entry)
263  {
264  var symbolKey = new SecurityDatabaseKey(market, symbol, securityType);
265  return _entries.TryGetValue(symbolKey, out entry)
266  // now check with null symbol key
267  || _entries.TryGetValue(symbolKey.CreateCommonKey(), out entry)
268  // if FOP check for future
269  || securityType == SecurityType.FutureOption && TryGetEntry(market,
270  FuturesOptionsSymbolMappings.MapFromOption(symbol), SecurityType.Future, out entry)
271  // if custom data type check for type specific entry
272  || (securityType == SecurityType.Base && SecurityIdentifier.TryGetCustomDataType(symbol, out var customType)
273  && _entries.TryGetValue(new SecurityDatabaseKey(market, $"TYPE.{customType}", securityType), out entry));
274  }
275 
276  /// <summary>
277  /// Gets the entry for the specified market/symbol/security-type
278  /// </summary>
279  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
280  /// <param name="symbol">The particular symbol being traded (Symbol class)</param>
281  /// <param name="securityType">The security type of the symbol</param>
282  /// <returns>The entry matching the specified market/symbol/security-type</returns>
283  public virtual Entry GetEntry(string market, Symbol symbol, SecurityType securityType)
284  {
285  return GetEntry(market, GetDatabaseSymbolKey(symbol), securityType);
286  }
287 
288  /// <summary>
289  /// Gets the correct string symbol to use as a database key
290  /// </summary>
291  /// <param name="symbol">The symbol</param>
292  /// <returns>The symbol string used in the database ke</returns>
293  public static string GetDatabaseSymbolKey(Symbol symbol)
294  {
295  string stringSymbol;
296  if (symbol == null)
297  {
298  stringSymbol = string.Empty;
299  }
300  else
301  {
302  switch (symbol.ID.SecurityType)
303  {
304  case SecurityType.Option:
305  stringSymbol = symbol.HasUnderlying ? symbol.Underlying.Value : string.Empty;
306  break;
307  case SecurityType.IndexOption:
308  case SecurityType.FutureOption:
309  stringSymbol = symbol.HasUnderlying ? symbol.ID.Symbol : string.Empty;
310  break;
311  case SecurityType.Base:
312  case SecurityType.Future:
313  stringSymbol = symbol.ID.Symbol;
314  break;
315  default:
316  stringSymbol = symbol.Value;
317  break;
318  }
319  }
320 
321  return stringSymbol;
322  }
323 
324  /// <summary>
325  /// Determines if the database contains the specified key
326  /// </summary>
327  /// <param name="key">The key to search for</param>
328  /// <returns>True if an entry is found, otherwise false</returns>
329  protected bool ContainsKey(SecurityDatabaseKey key)
330  {
331  return _entries.ContainsKey(key);
332  }
333 
334  /// <summary>
335  /// Represents a single entry in the <see cref="MarketHoursDatabase"/>
336  /// </summary>
337  public class Entry
338  {
339  /// <summary>
340  /// Gets the raw data time zone for this entry
341  /// </summary>
342  public readonly DateTimeZone DataTimeZone;
343  /// <summary>
344  /// Gets the exchange hours for this entry
345  /// </summary>
347  /// <summary>
348  /// Initializes a new instance of the <see cref="Entry"/> class
349  /// </summary>
350  /// <param name="dataTimeZone">The raw data time zone</param>
351  /// <param name="exchangeHours">The security exchange hours for this entry</param>
352  public Entry(DateTimeZone dataTimeZone, SecurityExchangeHours exchangeHours)
353  {
354  DataTimeZone = dataTimeZone;
355  ExchangeHours = exchangeHours;
356  }
357  }
358 
359  class AlwaysOpenMarketHoursDatabaseImpl : MarketHoursDatabase
360  {
361  public override Entry GetEntry(string market, string symbol, SecurityType securityType)
362  {
363  var key = new SecurityDatabaseKey(market, symbol, securityType);
364  var tz = ContainsKey(key)
365  ? base.GetEntry(market, symbol, securityType).ExchangeHours.TimeZone
366  : DateTimeZone.Utc;
367 
368  return new Entry(tz, SecurityExchangeHours.AlwaysOpen(tz));
369  }
370 
371  public AlwaysOpenMarketHoursDatabaseImpl()
372  : base(FromDataFolder().ExchangeHoursListing.ToDictionary())
373  {
374  }
375  }
376  }
377 }