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  internal 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  lock (DataFolderMarketHoursDatabaseLock)
185  {
186  _entries[key] = entry;
187  _customEntries[key] = entry;
188  }
189  return entry;
190  }
191 
192  /// <summary>
193  /// Convenience method for the common custom data case.
194  /// Sets the entry for the specified symbol using SecurityExchangeHours.AlwaysOpen(timeZone)
195  /// This sets the data time zone equal to the exchange time zone as well.
196  /// </summary>
197  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
198  /// <param name="symbol">The particular symbol being traded</param>
199  /// <param name="securityType">The security type of the symbol</param>
200  /// <param name="timeZone">The time zone of the symbol's exchange and raw data</param>
201  /// <returns>The entry matching the specified market/symbol/security-type</returns>
202  public virtual Entry SetEntryAlwaysOpen(string market, string symbol, SecurityType securityType, DateTimeZone timeZone)
203  {
204  return SetEntry(market, symbol, securityType, SecurityExchangeHours.AlwaysOpen(timeZone));
205  }
206 
207  /// <summary>
208  /// Gets the entry for the specified market/symbol/security-type
209  /// </summary>
210  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
211  /// <param name="symbol">The particular symbol being traded</param>
212  /// <param name="securityType">The security type of the symbol</param>
213  /// <returns>The entry matching the specified market/symbol/security-type</returns>
214  public virtual Entry GetEntry(string market, string symbol, SecurityType securityType)
215  {
216  Entry entry;
217  // Fall back on the Futures MHDB entry if the FOP lookup failed.
218  // Some FOPs have the same symbol properties as their futures counterparts.
219  // So, to save ourselves some space, we can fall back on the existing entries
220  // so that we don't duplicate the information.
221  if (!TryGetEntry(market, symbol, securityType, out entry))
222  {
223  var key = new SecurityDatabaseKey(market, symbol, securityType);
224  Log.Error($"MarketHoursDatabase.GetExchangeHours(): {Messages.MarketHoursDatabase.ExchangeHoursNotFound(key, _entries.Keys)}");
225 
226  if (securityType == SecurityType.Future && market == Market.USA)
227  {
229  if (SymbolPropertiesDatabase.FromDataFolder().TryGetMarket(symbol, SecurityType.Future, out market))
230  {
231  // let's suggest a market
232  exception += " " + Messages.MarketHoursDatabase.SuggestedMarketBasedOnTicker(market);
233  }
234 
235  throw new ArgumentException(exception);
236  }
237  // there was nothing that really matched exactly
238  throw new ArgumentException(Messages.MarketHoursDatabase.ExchangeHoursNotFound(key));
239  }
240 
241  return entry;
242  }
243 
244  /// <summary>
245  /// Tries to get the entry for the specified market/symbol/security-type
246  /// </summary>
247  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
248  /// <param name="symbol">The particular symbol being traded</param>
249  /// <param name="securityType">The security type of the symbol</param>
250  /// <param name="entry">The entry found if any</param>
251  /// <returns>True if the entry was present, else false</returns>
252  public bool TryGetEntry(string market, Symbol symbol, SecurityType securityType, out Entry entry)
253  {
254  return TryGetEntry(market, GetDatabaseSymbolKey(symbol), securityType, out entry);
255  }
256 
257  /// <summary>
258  /// Tries to get the entry for the specified market/symbol/security-type
259  /// </summary>
260  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
261  /// <param name="symbol">The particular symbol being traded</param>
262  /// <param name="securityType">The security type of the symbol</param>
263  /// <param name="entry">The entry found if any</param>
264  /// <returns>True if the entry was present, else false</returns>
265  public bool TryGetEntry(string market, string symbol, SecurityType securityType, out Entry entry)
266  {
267  var symbolKey = new SecurityDatabaseKey(market, symbol, securityType);
268  return _entries.TryGetValue(symbolKey, out entry)
269  // now check with null symbol key
270  || _entries.TryGetValue(symbolKey.CreateCommonKey(), out entry)
271  // if FOP check for future
272  || securityType == SecurityType.FutureOption && TryGetEntry(market,
273  FuturesOptionsSymbolMappings.MapFromOption(symbol), SecurityType.Future, out entry)
274  // if custom data type check for type specific entry
275  || (securityType == SecurityType.Base && SecurityIdentifier.TryGetCustomDataType(symbol, out var customType)
276  && _entries.TryGetValue(new SecurityDatabaseKey(market, $"TYPE.{customType}", securityType), out entry));
277  }
278 
279  /// <summary>
280  /// Gets the entry for the specified market/symbol/security-type
281  /// </summary>
282  /// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
283  /// <param name="symbol">The particular symbol being traded (Symbol class)</param>
284  /// <param name="securityType">The security type of the symbol</param>
285  /// <returns>The entry matching the specified market/symbol/security-type</returns>
286  public virtual Entry GetEntry(string market, Symbol symbol, SecurityType securityType)
287  {
288  return GetEntry(market, GetDatabaseSymbolKey(symbol), securityType);
289  }
290 
291  /// <summary>
292  /// Gets the correct string symbol to use as a database key
293  /// </summary>
294  /// <param name="symbol">The symbol</param>
295  /// <returns>The symbol string used in the database ke</returns>
296  public static string GetDatabaseSymbolKey(Symbol symbol)
297  {
298  string stringSymbol;
299  if (symbol == null)
300  {
301  stringSymbol = string.Empty;
302  }
303  else
304  {
305  switch (symbol.ID.SecurityType)
306  {
307  case SecurityType.Option:
308  stringSymbol = symbol.HasUnderlying ? symbol.Underlying.Value : string.Empty;
309  break;
310  case SecurityType.IndexOption:
311  case SecurityType.FutureOption:
312  stringSymbol = symbol.HasUnderlying ? symbol.ID.Symbol : string.Empty;
313  break;
314  case SecurityType.Base:
315  case SecurityType.Future:
316  stringSymbol = symbol.ID.Symbol;
317  break;
318  default:
319  stringSymbol = symbol.Value;
320  break;
321  }
322  }
323 
324  return stringSymbol;
325  }
326 
327  /// <summary>
328  /// Determines if the database contains the specified key
329  /// </summary>
330  /// <param name="key">The key to search for</param>
331  /// <returns>True if an entry is found, otherwise false</returns>
332  protected bool ContainsKey(SecurityDatabaseKey key)
333  {
334  return _entries.ContainsKey(key);
335  }
336 
337  /// <summary>
338  /// Represents a single entry in the <see cref="MarketHoursDatabase"/>
339  /// </summary>
340  public class Entry
341  {
342  /// <summary>
343  /// Gets the raw data time zone for this entry
344  /// </summary>
345  public DateTimeZone DataTimeZone { get; init; }
346  /// <summary>
347  /// Gets the exchange hours for this entry
348  /// </summary>
349  public SecurityExchangeHours ExchangeHours { get; init; }
350  /// <summary>
351  /// Initializes a new instance of the <see cref="Entry"/> class
352  /// </summary>
353  /// <param name="dataTimeZone">The raw data time zone</param>
354  /// <param name="exchangeHours">The security exchange hours for this entry</param>
355  public Entry(DateTimeZone dataTimeZone, SecurityExchangeHours exchangeHours)
356  {
357  DataTimeZone = dataTimeZone;
358  ExchangeHours = exchangeHours;
359  }
360  }
361 
362  class AlwaysOpenMarketHoursDatabaseImpl : MarketHoursDatabase
363  {
364  public override Entry GetEntry(string market, string symbol, SecurityType securityType)
365  {
366  var key = new SecurityDatabaseKey(market, symbol, securityType);
367  var tz = ContainsKey(key)
368  ? base.GetEntry(market, symbol, securityType).ExchangeHours.TimeZone
369  : DateTimeZone.Utc;
370 
371  return new Entry(tz, SecurityExchangeHours.AlwaysOpen(tz));
372  }
373 
374  public AlwaysOpenMarketHoursDatabaseImpl()
375  : base(FromDataFolder().ExchangeHoursListing.ToDictionary())
376  {
377  }
378  }
379  }
380 }