Files
2026-07-13 13:02:50 +08:00

562 lines
25 KiB
C#

/*
* 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.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using QuantConnect.Configuration;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
namespace QuantConnect.Tests.Common.Securities
{
[TestFixture]
public class MarketHoursDatabaseTests
{
[SetUp]
public void Setup()
{
MarketHoursDatabase.Reset();
}
[TearDown]
public void TearDown()
{
MarketHoursDatabase.Reset();
}
[Test]
public void InitializesFromFile()
{
string file = Path.Combine("TestData", "SampleMarketHoursDatabase.json");
var exchangeHours = GetMarketHoursDatabase(file);
Assert.AreEqual(3, exchangeHours.ExchangeHoursListing.Count);
}
[Test]
public void RetrievesExchangeHoursWithAndWithoutSymbol()
{
string file = Path.Combine("TestData", "SampleMarketHoursDatabase.json");
var exchangeHours = GetMarketHoursDatabase(file);
var hours = exchangeHours.GetExchangeHours(Market.USA, Symbols.SPY, SecurityType.Equity);
Assert.IsNotNull(hours);
Assert.AreEqual(hours, exchangeHours.GetExchangeHours(Market.USA, null, SecurityType.Equity));
}
[Test]
public void CorrectlyReadsClosedAllDayHours()
{
string file = Path.Combine("TestData", "SampleMarketHoursDatabase.json");
var exchangeHours = GetMarketHoursDatabase(file);
var hours = exchangeHours.GetExchangeHours(Market.USA, null, SecurityType.Equity);
Assert.IsNotNull(hours);
Assert.IsTrue(hours.MarketHours[DayOfWeek.Saturday].IsClosedAllDay);
}
[Test]
public void CorrectlyReadsOpenAllDayHours()
{
string file = Path.Combine("TestData", "SampleMarketHoursDatabase.json");
var exchangeHours = GetMarketHoursDatabase(file);
var hours = exchangeHours.GetExchangeHours(Market.FXCM, null, SecurityType.Forex);
Assert.IsNotNull(hours);
Assert.IsTrue(hours.MarketHours[DayOfWeek.Monday].IsOpenAllDay);
}
[Test]
public void InitializesFromDataFolder()
{
var provider = MarketHoursDatabase.FromDataFolder();
Assert.AreNotEqual(0, provider.ExchangeHoursListing.Count);
}
[Test]
public void CorrectlyReadsUsEquityMarketHours()
{
string file = Path.Combine("TestData", "SampleMarketHoursDatabase.json");
var exchangeHours = GetMarketHoursDatabase(file);
var equityHours = exchangeHours.GetExchangeHours(Market.USA, null, SecurityType.Equity);
foreach (var day in equityHours.MarketHours.Keys)
{
var marketHours = equityHours.MarketHours[day];
if (day == DayOfWeek.Saturday || day == DayOfWeek.Sunday)
{
Assert.IsTrue(marketHours.IsClosedAllDay);
continue;
}
Assert.AreEqual(new TimeSpan(4, 0, 0), marketHours.GetMarketOpen(TimeSpan.Zero, true));
Assert.AreEqual(new TimeSpan(9, 30, 0), marketHours.GetMarketOpen(TimeSpan.Zero, false));
Assert.AreEqual(new TimeSpan(16, 0, 0), marketHours.GetMarketClose(TimeSpan.Zero, false));
Assert.AreEqual(new TimeSpan(20, 0, 0), marketHours.GetMarketClose(TimeSpan.Zero, true));
}
}
[Test]
public void CorrectlyReadsUsEquityEarlyCloses()
{
string file = Path.Combine("TestData", "SampleMarketHoursDatabase.json");
var exchangeHours = GetMarketHoursDatabase(file);
var equityHours = exchangeHours.GetExchangeHours(Market.USA, null, SecurityType.Equity);
Assert.AreNotEqual(0, equityHours.EarlyCloses.Count);
var date = new DateTime(2016, 11, 25);
var earlyCloseTime = new TimeSpan(13, 0, 0);
Assert.AreEqual(earlyCloseTime, equityHours.EarlyCloses[date]);
}
[TestCase("AUP", Market.COMEX, true)]
[TestCase("AA6", Market.NYMEX, true)]
[TestCase("6A", Market.CME, false)]
[TestCase("30Y", Market.CBOT, false)]
[TestCase("HE", Market.CME, true)]
[TestCase("AW", Market.CBOT, true)]
[TestCase("HE", Market.CME, true)]
[TestCase("AW", Market.CBOT, true)]
[TestCase("LE", Market.CME, true)]
[TestCase("BCF", Market.CBOT, true)]
[TestCase("GD", Market.CME, true)]
[TestCase("BWF", Market.CBOT, true)]
[TestCase("CSC", Market.CME, true)]
[TestCase("GNF", Market.CME, true)]
[TestCase("GDK", Market.CME, true)]
public void CorrectlyReadsCMEGroupFutureHolidayGoodFridaySchedule(string futureTicker, string market, bool isHoliday)
{
var provider = MarketHoursDatabase.FromDataFolder();
var ticker = OptionSymbol.MapToUnderlying(futureTicker, SecurityType.Future);
var future = Symbol.Create(ticker, SecurityType.Future, market);
var futureEntry = provider.GetEntry(market, ticker, future.SecurityType);
var holidays = futureEntry.ExchangeHours.Holidays;
var holidayDate = DateTime.Parse("4/7/2023", CultureInfo.InvariantCulture);
Assert.AreEqual(isHoliday, holidays.Contains(holidayDate));
}
[TestCase("2YY", Market.CBOT, "4/7/2023", true)]
[TestCase("TN", Market.CBOT, "4/7/2023", true)]
[TestCase("6A", Market.CME, "4/7/2023", true)]
[TestCase("6Z", Market.CME, "4/7/2023", true)]
[TestCase("M6A", Market.CME, "4/7/2023", true)]
[TestCase("MCD", Market.CME, "4/7/2023", true)]
[TestCase("AW", Market.CBOT, "4/6/2023", false)]
[TestCase("BCF", Market.CBOT, "4/6/2023", false)]
[TestCase("BWF", Market.CBOT, "4/6/2023", false)]
[TestCase("ZC", Market.CBOT, "4/6/2023", false)]
[TestCase("DC", Market.CME, "4/7/2023", false)]
[TestCase("DY", Market.CME, "4/7/2023", false)]
[TestCase("GNF", Market.CME, "4/6/2023", true)]
[TestCase("GDK", Market.CME, "4/6/2023", true)]
public void CorrectlyReadsCMEGroupFutureEarlyCloses(string futureTicker, string market, string date, bool isEarlyClose)
{
var provider = MarketHoursDatabase.FromDataFolder();
var ticker = OptionSymbol.MapToUnderlying(futureTicker, SecurityType.Future);
var future = Symbol.Create(ticker, SecurityType.Future, market);
var futureEntry = provider.GetEntry(market, ticker, future.SecurityType);
var earlyCloses = futureEntry.ExchangeHours.EarlyCloses;
var earlyCloseDate = DateTime.Parse(date, CultureInfo.InvariantCulture);
Assert.AreEqual(isEarlyClose, earlyCloses.Keys.Contains(earlyCloseDate));
if (isEarlyClose)
{
var holidays = futureEntry.ExchangeHours.Holidays;
Assert.IsFalse(holidays.Contains(earlyCloseDate));
}
}
[TestCase("BIO", Market.CME, true)]
[TestCase("5YY", Market.CBOT, true)]
[TestCase("6E", Market.CME, true)]
[TestCase("BTC", Market.CME, true)]
[TestCase("A8O", Market.NYMEX, true)]
[TestCase("PAM", Market.NYMEX, true)]
[TestCase("ZC", Market.CBOT, false)]
[TestCase("LBR", Market.CME, false)]
[TestCase("HE", Market.CME, false)]
[TestCase("DY", Market.CME, false)]
[TestCase("YO", Market.NYMEX, true)]
public void CorrectlyReadsCMEGroupFutureBankHolidays(string futureTicker, string market, bool isBankHoliday)
{
var provider = MarketHoursDatabase.FromDataFolder();
var future = Symbol.Create(futureTicker, SecurityType.Future, market);
var futureEntry = provider.GetEntry(market, future, future.SecurityType);
var bankHolidays = futureEntry.ExchangeHours.BankHolidays;
var bankHoliday = new DateTime(2025, 11, 27);
Assert.AreEqual(isBankHoliday, bankHolidays.Contains(bankHoliday));
Assert.AreEqual(isBankHoliday, futureEntry.ExchangeHours.IsDateOpen(bankHoliday, extendedMarketHours: true));
}
[TestCase("2YY", Market.CBOT, true)]
[TestCase("TN", Market.CBOT, true)]
[TestCase("6A", Market.CME, true)]
[TestCase("6Z", Market.CME, true)]
[TestCase("M6A", Market.CME, true)]
[TestCase("MCD", Market.CME, true)]
[TestCase("AW", Market.CBOT, false)]
[TestCase("BCF", Market.CBOT, false)]
[TestCase("BWF", Market.CBOT, false)]
[TestCase("ZC", Market.CBOT, false)]
[TestCase("DC", Market.CME, false)]
[TestCase("DY", Market.CME, false)]
[TestCase("GNF", Market.CME, false)]
[TestCase("GDK", Market.CME, false)]
public void CheckJustEarlyClosesOrJustHolidaysForCMEGroupFuturesOnGoodFriday(string futureTicker, string market, bool isEarlyClose)
{
var provider = MarketHoursDatabase.FromDataFolder();
var ticker = OptionSymbol.MapToUnderlying(futureTicker, SecurityType.Future);
var future = Symbol.Create(ticker, SecurityType.Future, market);
var futureEntry = provider.GetEntry(market, ticker, future.SecurityType);
var earlyCloses = futureEntry.ExchangeHours.EarlyCloses;
var holidays = futureEntry.ExchangeHours.Holidays;
var goodFriday = DateTime.Parse("4/7/2023", CultureInfo.InvariantCulture);
Assert.AreEqual(isEarlyClose, earlyCloses.Keys.Contains(goodFriday));
Assert.AreEqual(!isEarlyClose, holidays.Contains(goodFriday));
}
[TestCase("ES", Market.CME)]
[TestCase("30Y", Market.CBOT)]
[TestCase("M6B", Market.CME)]
[TestCase("BTC", Market.CME)]
[TestCase("ABT", Market.NYMEX)]
[TestCase("AUP", Market.COMEX)]
public void EarlyClosesResumesAgainIfLateOpen(string futureTicker, string market)
{
var provider = MarketHoursDatabase.FromDataFolder();
var ticker = OptionSymbol.MapToUnderlying(futureTicker, SecurityType.Future);
var future = Symbol.Create(ticker, SecurityType.Future, market);
var futureEntry = provider.GetEntry(market, ticker, future.SecurityType);
var earlyCloseDate = DateTime.Parse("9/4/2023", CultureInfo.InvariantCulture);
var earlyCloseHour = futureEntry.ExchangeHours.EarlyCloses[earlyCloseDate];
var lateOpenHour = futureEntry.ExchangeHours.LateOpens[earlyCloseDate];
Assert.AreEqual(earlyCloseHour, futureEntry.ExchangeHours.GetMarketHours(earlyCloseDate).GetMarketClose(new TimeSpan(0, 0, 0), true));
Assert.AreEqual(lateOpenHour, futureEntry.ExchangeHours.GetMarketHours(earlyCloseDate).GetMarketOpen(earlyCloseHour, true));
}
[Test]
public void CorrectlyReadFxcmForexMarketHours()
{
string file = Path.Combine("TestData", "SampleMarketHoursDatabase.json");
var exchangeHours = GetMarketHoursDatabase(file);
var equityHours = exchangeHours.GetExchangeHours(Market.FXCM, null, SecurityType.Forex);
foreach (var day in equityHours.MarketHours.Keys)
{
var marketHours = equityHours.MarketHours[day];
if (day == DayOfWeek.Saturday)
{
Assert.IsTrue(marketHours.IsClosedAllDay);
}
else if (day != DayOfWeek.Sunday && day != DayOfWeek.Friday)
{
Assert.IsTrue(marketHours.IsOpenAllDay);
}
else if (day == DayOfWeek.Sunday)
{
Assert.AreEqual(new TimeSpan(17, 0, 0), marketHours.GetMarketOpen(TimeSpan.Zero, true));
Assert.AreEqual(new TimeSpan(17, 0, 0), marketHours.GetMarketOpen(TimeSpan.Zero, false));
Assert.AreEqual(new TimeSpan(24, 0, 0), marketHours.GetMarketClose(TimeSpan.Zero, false));
Assert.AreEqual(new TimeSpan(24, 0, 0), marketHours.GetMarketClose(TimeSpan.Zero, true));
}
else
{
Assert.AreEqual(new TimeSpan(0, 0, 0), marketHours.GetMarketOpen(TimeSpan.Zero, true));
Assert.AreEqual(new TimeSpan(0, 0, 0), marketHours.GetMarketOpen(TimeSpan.Zero, false));
Assert.AreEqual(new TimeSpan(17, 0, 0), marketHours.GetMarketClose(TimeSpan.Zero, false));
Assert.AreEqual(new TimeSpan(17, 0, 0), marketHours.GetMarketClose(TimeSpan.Zero, true));
}
}
}
[Test]
public void ReadsUsEquityDataTimeZone()
{
string file = Path.Combine("TestData", "SampleMarketHoursDatabase.json");
var marketHoursDatabase = GetMarketHoursDatabase(file);
Assert.AreEqual(TimeZones.NewYork, marketHoursDatabase.GetDataTimeZone(Market.USA, null, SecurityType.Equity));
}
[Test]
public void AllMarketsAreAlwaysOpenWhenForceExchangeAlwaysOpenIsTrue()
{
var originalConfigValue = Config.Get("force-exchange-always-open");
// Force all exchanges to be treated as always open, regardless of their actual hours
Config.Set("force-exchange-always-open", "true");
string file = Path.Combine("TestData", "SampleMarketHoursDatabase.json");
var marketHoursDatabase = GetMarketHoursDatabase(file);
foreach (var entry in marketHoursDatabase.ExchangeHoursListing)
{
var key = entry.Key;
var exchangeHours = marketHoursDatabase.GetExchangeHours(key.Market, key.Symbol, key.SecurityType);
// Assert that the market is considered always open under this configuration
Assert.IsTrue(exchangeHours.IsMarketAlwaysOpen);
}
// Restore original config value after test
Config.Set("force-exchange-always-open", originalConfigValue);
}
[Test]
public void ReadsFxcmForexDataTimeZone()
{
string file = Path.Combine("TestData", "SampleMarketHoursDatabase.json");
var marketHoursDatabase = GetMarketHoursDatabase(file);
Assert.AreEqual(TimeZones.EasternStandard, marketHoursDatabase.GetDataTimeZone(Market.FXCM, null, SecurityType.Forex));
}
[TestCase("SPX", SecurityType.Index, Market.USA)]
[TestCase("SPXW", SecurityType.Index, Market.USA)]
[TestCase("AAPL", SecurityType.Equity, Market.USA)]
[TestCase("SPY", SecurityType.Equity, Market.USA)]
[TestCase("GC", SecurityType.Future, Market.COMEX)]
[TestCase("SI", SecurityType.Future, Market.COMEX)]
[TestCase("HG", SecurityType.Future, Market.COMEX)]
[TestCase("ES", SecurityType.Future, Market.CME)]
[TestCase("NQ", SecurityType.Future, Market.CME)]
[TestCase("CL", SecurityType.Future, Market.NYMEX)]
[TestCase("NG", SecurityType.Future, Market.NYMEX)]
[TestCase("ZB", SecurityType.Future, Market.CBOT)]
[TestCase("ZC", SecurityType.Future, Market.CBOT)]
[TestCase("ZS", SecurityType.Future, Market.CBOT)]
[TestCase("ZT", SecurityType.Future, Market.CBOT)]
[TestCase("ZW", SecurityType.Future, Market.CBOT)]
public void MissingOptionsEntriesResolveToUnderlyingMarketHours(string optionTicker, SecurityType securityType, string market)
{
var provider = MarketHoursDatabase.FromDataFolder();
var underlyingTIcker = OptionSymbol.MapToUnderlying(optionTicker, securityType);
var underlying = Symbol.Create(underlyingTIcker, securityType, market);
var option = Symbol.CreateOption(
underlying,
market,
default,
default,
default,
SecurityIdentifier.DefaultDate);
var underlyingEntry = provider.GetEntry(market, underlying, underlying.SecurityType);
var optionEntry = provider.GetEntry(market, option, option.SecurityType);
if (securityType == SecurityType.Future)
{
Assert.AreEqual(underlyingEntry, optionEntry);
}
else
{
Assert.AreEqual(underlyingEntry.ExchangeHours.Holidays, optionEntry.ExchangeHours.Holidays);
Assert.AreEqual(underlyingEntry.ExchangeHours.LateOpens, optionEntry.ExchangeHours.LateOpens);
Assert.AreEqual(underlyingEntry.ExchangeHours.EarlyCloses, optionEntry.ExchangeHours.EarlyCloses);
}
}
[TestCase("SPX")]
[TestCase("NDX")]
[TestCase("VIX")]
public void USIndexOptionsResolveToUnderlyingEarlyCloses(string optionTicker)
{
var provider = MarketHoursDatabase.FromDataFolder();
var underlyingTicker = OptionSymbol.MapToUnderlying(optionTicker, SecurityType.Index);
var underlying = Symbol.Create(underlyingTicker, SecurityType.Index, Market.USA);
var option = Symbol.CreateOption(
underlying,
Market.USA,
default,
default,
default,
SecurityIdentifier.DefaultDate);
var underlyingEntry = provider.GetEntry(Market.USA, underlying, underlying.SecurityType);
var optionEntry = provider.GetEntry(Market.USA, option, option.SecurityType);
Assert.AreEqual(underlyingEntry.ExchangeHours.EarlyCloses, optionEntry.ExchangeHours.EarlyCloses);
}
[TestCase("GC", Market.COMEX, "OG")]
[TestCase("SI", Market.COMEX, "SO")]
[TestCase("HG", Market.COMEX, "HXE")]
[TestCase("ES", Market.CME, "ES")]
[TestCase("NQ", Market.CME, "NQ")]
[TestCase("CL", Market.NYMEX, "LO")]
[TestCase("NG", Market.NYMEX, "ON")]
[TestCase("ZB", Market.CBOT, "OZB")]
[TestCase("ZC", Market.CBOT, "OZC")]
[TestCase("ZS", Market.CBOT, "OZS")]
[TestCase("ZT", Market.CBOT, "OZT")]
[TestCase("ZW", Market.CBOT, "OZW")]
public void FuturesOptionsGetDatabaseSymbolKey(string ticker, string market, string expected)
{
var future = Symbol.Create(ticker, SecurityType.Future, market);
var option = Symbol.CreateOption(
future,
market,
default(OptionStyle),
default(OptionRight),
default(decimal),
SecurityIdentifier.DefaultDate);
Assert.AreEqual(expected, MarketHoursDatabase.GetDatabaseSymbolKey(option));
}
[Test]
public void CustomEntriesStoredAndFetched()
{
var database = MarketHoursDatabase.FromDataFolder();
var ticker = "BTC";
var hours = SecurityExchangeHours.AlwaysOpen(TimeZones.Berlin);
var entry = database.SetEntry(Market.USA, ticker, SecurityType.Base, hours);
// Assert our hours match the result
Assert.AreEqual(hours, entry.ExchangeHours);
// Fetch the entry to ensure we can access it with the ticker
var fetchedEntry = database.GetEntry(Market.USA, ticker, SecurityType.Base);
Assert.AreSame(entry, fetchedEntry);
}
[TestCase("UWU", SecurityType.Base)]
[TestCase("SPX", SecurityType.Index)]
public void CustomEntriesAreNotLostWhenReset(string ticker, SecurityType securityType)
{
var database = MarketHoursDatabase.FromDataFolder();
var hours = SecurityExchangeHours.AlwaysOpen(TimeZones.Chicago);
var entry = database.SetEntry(Market.USA, ticker, securityType, hours);
MarketHoursDatabase.Entry returnedEntry;
Assert.IsTrue(database.TryGetEntry(Market.USA, ticker, securityType, out returnedEntry));
Assert.AreEqual(returnedEntry, entry);
Assert.DoesNotThrow(() => database.UpdateDataFolderDatabase());
Assert.IsTrue(database.TryGetEntry(Market.USA, ticker, securityType, out returnedEntry));
Assert.AreEqual(returnedEntry, entry);
}
[TestCase("VIX3M")]
[TestCase("VVIX")]
[TestCase("TESTIDX")]
public void CorrectlyReadsCBOEIndexMarketHours(string ticker)
{
var db = MarketHoursDatabase.FromDataFolder();
var symbol = Symbol.Create(ticker, SecurityType.Index, Market.CBOE);
Assert.IsTrue(db.TryGetEntry(Market.CBOE, symbol, SecurityType.Index, out var entry));
Assert.AreEqual(TimeZones.Chicago, entry.ExchangeHours.TimeZone);
Assert.AreEqual(TimeZones.NewYork, entry.DataTimeZone);
var weekdays = new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday };
foreach (var day in weekdays)
{
var marketSegment = entry.ExchangeHours.MarketHours[day].Segments.First(s => s.State == MarketHoursState.Market);
Assert.AreEqual(new TimeSpan(8, 30, 0), marketSegment.Start);
Assert.AreEqual(new TimeSpan(15, 15, 0), marketSegment.End);
}
}
[Test]
public void VerifyMarketHoursDataIntegrityForAllSymbols()
{
// Load the market hours database
var marketHoursDatabase = MarketHoursDatabase.FromDataFolder();
// Test all specific entries in parallel
Parallel.ForEach(marketHoursDatabase.ExchangeHoursListing, entry =>
{
var securityType = entry.Key.SecurityType;
var ticker = entry.Key.Symbol;
Assert.IsFalse(string.IsNullOrEmpty(ticker), $"Ticker is null or empty");
var market = entry.Key.Market;
// Create symbol
Symbol symbol;
if (ticker.Contains("[*]") || ticker == "*")
{
symbol = Symbol.Create("TEST_SYMBOL", securityType, market);
}
else
{
symbol = Symbol.Create(ticker, securityType, market);
}
TestMarketHoursForSymbol(marketHoursDatabase, market, symbol, securityType);
});
}
private static void TestMarketHoursForSymbol(MarketHoursDatabase marketHoursDatabase, string market, Symbol symbol, SecurityType securityType)
{
// Define date range (1998-01-01 to today, checking daily)
var startDate = new DateTime(1998, 1, 1);
var endDate = DateTime.Now;
var exchangeHours = marketHoursDatabase.GetExchangeHours(market, symbol, securityType);
// Check every day
for (var date = startDate; date <= endDate; date = date.AddDays(1))
{
// Get market hours for this date
var marketHours = exchangeHours.GetMarketHours(date);
// Ensure market hours exist for the date
Assert.IsNotNull(exchangeHours, "Exchange hours should not be null.");
var segments = marketHours.Segments;
for (int i = 1; i < segments.Count; i++)
{
// Ensure segments do not overlap
Assert.LessOrEqual(segments[i - 1].End, segments[i].Start,
$"Segments overlap for {symbol} on {date:yyyy-MM-dd} between {segments[i - 1]} and {segments[i]}");
}
bool hasEarlyClose = exchangeHours.EarlyCloses.TryGetValue(date, out var earlyCloseTime);
bool hasLateOpen = exchangeHours.LateOpens.TryGetValue(date, out var lateOpenTime);
if (hasEarlyClose && hasLateOpen && segments.Count > 0)
{
// Ensure LateOpen time is not after market close, but only when there is an EarlyClose
Assert.LessOrEqual(lateOpenTime, segments[^1].End,
$"Late open time {lateOpenTime} is after market close {segments[^1].End} for {symbol} on {date:yyyy-MM-dd}");
}
if (exchangeHours.Holidays.Contains(date))
{
// Ensure market is fully closed on holidays
Assert.IsTrue(marketHours.IsClosedAllDay,
$"Market should be fully closed on holiday {date:yyyy-MM-dd} for {symbol}");
}
}
}
private static MarketHoursDatabase GetMarketHoursDatabase(string file)
{
return MarketHoursDatabase.FromFile(file);
}
}
}