/*
* 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())
{
}
}
}
}