/* * 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.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json; using NodaTime; using QuantConnect.Data; using QuantConnect.Logging; using QuantConnect.Util; namespace QuantConnect.Securities { /// /// Provides access to exchange hours and raw data times zones in various markets /// [JsonConverter(typeof(MarketHoursDatabaseJsonConverter))] public class MarketHoursDatabase { private static MarketHoursDatabase _dataFolderMarketHoursDatabase; private static readonly object DataFolderMarketHoursDatabaseLock = new object(); private readonly Dictionary _entries; /// /// Gets all the exchange hours held by this provider /// public List> ExchangeHoursListing => _entries.ToList(); /// /// Gets a that always returns /// public static MarketHoursDatabase AlwaysOpen { get; } = new AlwaysOpenMarketHoursDatabaseImpl(); /// /// Initializes a new instance of the class /// /// The full listing of exchange hours by key public MarketHoursDatabase(IReadOnlyDictionary exchangeHours) { _entries = exchangeHours.ToDictionary(); } /// /// Convenience method for retrieving exchange hours from market hours database using a subscription config /// /// The subscription data config to get exchange hours for /// The configure exchange hours for the specified configuration public SecurityExchangeHours GetExchangeHours(SubscriptionDataConfig configuration) { return GetExchangeHours(configuration.Market, configuration.Symbol, configuration.SecurityType); } /// /// Convenience method for retrieving exchange hours from market hours database using a subscription config /// /// The market the exchange resides in, i.e, 'usa', 'fxcm', ect... /// The particular symbol being traded /// The security type of the symbol /// The exchange hours for the specified security public SecurityExchangeHours GetExchangeHours(string market, Symbol symbol, SecurityType securityType) { return GetEntry(market, symbol, securityType).ExchangeHours; } /// /// Performs a lookup using the specified information and returns the data's time zone if found, /// if an entry is not found, an exception is thrown /// /// The market the exchange resides in, i.e, 'usa', 'fxcm', ect... /// The particular symbol being traded /// The security type of the symbol /// The raw data time zone for the specified security public DateTimeZone GetDataTimeZone(string market, Symbol symbol, SecurityType securityType) { var stringSymbol = symbol == null ? string.Empty : symbol.Value; return GetEntry(market, stringSymbol, securityType).DataTimeZone; } /// /// Resets the market hours database, forcing a reload when reused. /// Called in tests where multiple algorithms are run sequentially, /// and we need to guarantee that every test starts with the same environment. /// public static void Reset() { lock (DataFolderMarketHoursDatabaseLock) { _dataFolderMarketHoursDatabase = null; } } /// /// Gets the instance of the class produced by reading in the market hours /// data found in /Data/market-hours/ /// /// A class that represents the data in the market-hours folder public static MarketHoursDatabase FromDataFolder() { return FromDataFolder(Globals.DataFolder); } /// /// Gets the instance of the class produced by reading in the market hours /// data found in /Data/market-hours/ /// /// Path to the data folder /// A class that represents the data in the market-hours folder public static MarketHoursDatabase FromDataFolder(string dataFolder) { lock (DataFolderMarketHoursDatabaseLock) { if (_dataFolderMarketHoursDatabase == null) { var path = Path.Combine(dataFolder, "market-hours", "market-hours-database.json"); _dataFolderMarketHoursDatabase = FromFile(path); } } return _dataFolderMarketHoursDatabase; } /// /// Reads the specified file as a market hours database instance /// /// The market hours database file path /// A new instance of the class public static MarketHoursDatabase FromFile(string path) { return JsonConvert.DeserializeObject(File.ReadAllText(path)); } /// /// Sets the entry for the specified market/symbol/security-type. /// This is intended to be used by custom data and other data sources that don't have explicit /// entries in market-hours-database.csv. At run time, the algorithm can update the market hours /// database via calls to AddData. /// /// The market the exchange resides in, i.e, 'usa', 'fxcm', ect... /// The particular symbol being traded /// The security type of the symbol /// The exchange hours for the specified symbol /// The time zone of the symbol's raw data. Optional, defaults to the exchange time zone /// The entry matching the specified market/symbol/security-type public virtual Entry SetEntry(string market, string symbol, SecurityType securityType, SecurityExchangeHours exchangeHours, DateTimeZone dataTimeZone = null) { dataTimeZone = dataTimeZone ?? exchangeHours.TimeZone; var key = new SecurityDatabaseKey(market, symbol, securityType); var entry = new Entry(dataTimeZone, exchangeHours); _entries[key] = entry; return entry; } /// /// Convenience method for the common custom data case. /// Sets the entry for the specified symbol using SecurityExchangeHours.AlwaysOpen(timeZone) /// This sets the data time zone equal to the exchange time zone as well. /// /// The market the exchange resides in, i.e, 'usa', 'fxcm', ect... /// The particular symbol being traded /// The security type of the symbol /// The time zone of the symbol's exchange and raw data /// The entry matching the specified market/symbol/security-type public virtual Entry SetEntryAlwaysOpen(string market, string symbol, SecurityType securityType, DateTimeZone timeZone) { return SetEntry(market, symbol, securityType, SecurityExchangeHours.AlwaysOpen(timeZone)); } /// /// Gets the entry for the specified market/symbol/security-type /// /// The market the exchange resides in, i.e, 'usa', 'fxcm', ect... /// The particular symbol being traded /// The security type of the symbol /// The entry matching the specified market/symbol/security-type public virtual Entry GetEntry(string market, string symbol, SecurityType securityType) { Entry entry; var key = new SecurityDatabaseKey(market, symbol, securityType); if (!_entries.TryGetValue(key, out entry)) { // now check with null symbol key if (!_entries.TryGetValue(new SecurityDatabaseKey(market, null, securityType), out entry)) { var keys = string.Join(", ", _entries.Keys); Log.Error($"MarketHoursDatabase.GetExchangeHours(): Unable to locate exchange hours for {key}.Available keys: {keys}"); // there was nothing that really matched exactly... what should we do here? throw new ArgumentException("Unable to locate exchange hours for " + key); } } return entry; } /// /// Gets the entry for the specified market/symbol/security-type /// /// The market the exchange resides in, i.e, 'usa', 'fxcm', ect... /// The particular symbol being traded (Symbol class) /// The security type of the symbol /// The entry matching the specified market/symbol/security-type public virtual Entry GetEntry(string market, Symbol symbol, SecurityType securityType) { return GetEntry(market, GetDatabaseSymbolKey(symbol), securityType); } /// /// Gets the correct string symbol to use as a database key /// /// The symbol /// The symbol string used in the database ke public static string GetDatabaseSymbolKey(Symbol symbol) { string stringSymbol; if (symbol == null) { stringSymbol = string.Empty; } else { switch (symbol.ID.SecurityType) { case SecurityType.Option: stringSymbol = symbol.HasUnderlying ? symbol.Underlying.Value : string.Empty; break; default: stringSymbol = symbol.ID.SecurityType == SecurityType.Future ? symbol.ID.Symbol : symbol.Value; break; } } return stringSymbol; } /// /// Determines if the database contains the specified key /// /// The key to search for /// True if an entry is found, otherwise false protected bool ContainsKey(SecurityDatabaseKey key) { return _entries.ContainsKey(key); } /// /// Represents a single entry in the /// public class Entry { /// /// Gets the raw data time zone for this entry /// public readonly DateTimeZone DataTimeZone; /// /// Gets the exchange hours for this entry /// public readonly SecurityExchangeHours ExchangeHours; /// /// Initializes a new instance of the class /// /// The raw data time zone /// The security exchange hours for this entry public Entry(DateTimeZone dataTimeZone, SecurityExchangeHours exchangeHours) { DataTimeZone = dataTimeZone; ExchangeHours = exchangeHours; } } class AlwaysOpenMarketHoursDatabaseImpl : MarketHoursDatabase { public override Entry GetEntry(string market, string symbol, SecurityType securityType) { var key = new SecurityDatabaseKey(market, symbol, securityType); var tz = ContainsKey(key) ? base.GetEntry(market, symbol, securityType).ExchangeHours.TimeZone : DateTimeZone.Utc; return new Entry(tz, SecurityExchangeHours.AlwaysOpen(tz)); } public AlwaysOpenMarketHoursDatabaseImpl() : base(FromDataFolder().ExchangeHoursListing.ToDictionary()) { } } } }