Skip to content

Commit 9be02d7

Browse files
Add bank holidays for CME Futures MHDB (#8539)
* Add bank holidays * Add missing holidays to MHDB * Add 2025 new year's eve bank holiday * Add Columbus day and veterans day to bank holidays * Add missing bank holidays entry * Modify FuturesExpiryFunctions * Add missing change * Remove empty bankHolidays lists from MHDB * Address review * Add tests * Address review * Add bank holidays from 2009-2024 * Fix unit and regression tests * Review bank holidays * Fix tests * MHDB tweaks * Remove Bank holidays from CNH, MNH and MIR * Add setting for bank holidays disabled by default --------- Co-authored-by: Martin Molinero <martin.molinero1@gmail.com>
1 parent 55c995e commit 9be02d7

File tree

10 files changed

+26974
-596
lines changed

10 files changed

+26974
-596
lines changed

Common/Securities/Future/FuturesExpiryFunctions.cs

Lines changed: 204 additions & 206 deletions
Large diffs are not rendered by default.

Common/Securities/Future/FuturesExpiryUtilityFunctions.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,26 @@ public static class FuturesExpiryUtilityFunctions
3838
"GNF"
3939
};
4040

41+
/// <summary>
42+
/// True to account for bank holidays which will adjust futures expiration dates
43+
/// </summary>
44+
public static bool BankHolidays { get; set; }
45+
4146
/// <summary>
4247
/// Get holiday list from the MHDB given the market and the symbol of the security
4348
/// </summary>
4449
/// <param name="market">The market the exchange resides in, i.e, 'usa', 'fxcm', ect...</param>
4550
/// <param name="symbol">The particular symbol being traded</param>s
46-
public static HashSet<DateTime> GetHolidays(string market, string symbol)
51+
internal static HashSet<DateTime> GetExpirationHolidays(string market, string symbol)
4752
{
48-
return MarketHoursDatabase.FromDataFolder()
53+
var exchangeHours = MarketHoursDatabase.FromDataFolder()
4954
.GetEntry(market, symbol, SecurityType.Future)
50-
.ExchangeHours
51-
.Holidays;
55+
.ExchangeHours;
56+
if (BankHolidays)
57+
{
58+
return exchangeHours.Holidays.Concat(exchangeHours.BankHolidays).ToHashSet();
59+
}
60+
return exchangeHours.Holidays;
5261
}
5362

5463
/// <summary>

Common/Securities/FutureOption/FuturesOptionsExpiryFunctions.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ public static class FuturesOptionsExpiryFunctions
4646
// Trading terminates 7 business days before the 26th calendar of the month prior to the contract month. https://www.cmegroup.com/trading/energy/crude-oil/light-sweet-crude_contractSpecs_options.html#optionProductId=190
4747
{_lo, expiryMonth => {
4848
var twentySixthDayOfPreviousMonthFromContractMonth = expiryMonth.AddMonths(-1).AddDays(-(expiryMonth.Day - 1)).AddDays(25);
49-
var holidays = _mhdb.GetEntry(_lo.ID.Market, _lo.Underlying, SecurityType.Future)
50-
.ExchangeHours
51-
.Holidays;
49+
var holidays = FuturesExpiryUtilityFunctions.GetExpirationHolidays(_lo.ID.Market, _lo.Underlying.ID.Symbol);
5250

5351
return FuturesExpiryUtilityFunctions.AddBusinessDays(twentySixthDayOfPreviousMonthFromContractMonth, -7, holidays);
5452
}},
@@ -134,15 +132,13 @@ public static DateTime GetFutureOptionExpiryFromFutureExpiry(Symbol futureSymbol
134132
/// <returns>Expiry DateTime of the Future Option</returns>
135133
private static DateTime FridayBeforeTwoBusinessDaysBeforeEndOfMonth(Symbol underlyingFuture, DateTime expiryMonth)
136134
{
137-
var holidays = _mhdb.GetEntry(underlyingFuture.ID.Market, underlyingFuture, SecurityType.Future)
138-
.ExchangeHours
139-
.Holidays;
135+
var holidays = FuturesExpiryUtilityFunctions.GetExpirationHolidays(underlyingFuture.ID.Market, underlyingFuture.ID.Symbol);
140136

141137
var expiryMonthPreceding = expiryMonth.AddMonths(-1).AddDays(-(expiryMonth.Day - 1));
142138
var fridayBeforeSecondLastBusinessDay = FuturesExpiryUtilityFunctions.NthLastBusinessDay(
143139
expiryMonthPreceding,
144140
2,
145-
holidayList: holidays).AddDays(-1);
141+
holidays).AddDays(-1);
146142

147143
while (fridayBeforeSecondLastBusinessDay.DayOfWeek != DayOfWeek.Friday)
148144
{
@@ -165,12 +161,10 @@ private static DateTime FridayBeforeTwoBusinessDaysBeforeEndOfMonth(Symbol under
165161
/// <returns>Expiry DateTime of the Future Option</returns>
166162
private static DateTime FourthLastBusinessDayInPrecedingMonthFromContractMonth(Symbol underlyingFuture, DateTime expiryMonth, int hour, int minutes, bool noFridays = true)
167163
{
168-
var holidays = _mhdb.GetEntry(underlyingFuture.ID.Market, underlyingFuture, SecurityType.Future)
169-
.ExchangeHours
170-
.Holidays;
164+
var holidays = FuturesExpiryUtilityFunctions.GetExpirationHolidays(underlyingFuture.ID.Market, underlyingFuture.ID.Symbol);
171165

172166
var expiryMonthPreceding = expiryMonth.AddMonths(-1);
173-
var fourthLastBusinessDay = FuturesExpiryUtilityFunctions.NthLastBusinessDay(expiryMonthPreceding, 4, holidayList: holidays);
167+
var fourthLastBusinessDay = FuturesExpiryUtilityFunctions.NthLastBusinessDay(expiryMonthPreceding, 4, holidays);
174168

175169
if (noFridays)
176170
{

Common/Securities/SecurityExchangeHours.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ namespace QuantConnect.Securities
3333
public class SecurityExchangeHours
3434
{
3535
private HashSet<long> _holidays;
36+
private HashSet<long> _bankHolidays;
3637
private IReadOnlyDictionary<DateTime, TimeSpan> _earlyCloses;
3738
private IReadOnlyDictionary<DateTime, TimeSpan> _lateOpens;
3839

@@ -68,6 +69,16 @@ public HashSet<DateTime> Holidays
6869
get { return _holidays.ToHashSet(x => new DateTime(x)); }
6970
}
7071

72+
/// <summary>
73+
/// Gets the bank holidays for the exchange
74+
/// </summary>
75+
/// <remarks>In some markets and assets, like CME futures, there are tradable dates (market open) which
76+
/// should not be considered for expiration rules due to banks being closed</remarks>
77+
public HashSet<DateTime> BankHolidays
78+
{
79+
get { return _bankHolidays.ToHashSet(x => new DateTime(x)); }
80+
}
81+
7182
/// <summary>
7283
/// Gets the market hours for this exchange
7384
/// </summary>
@@ -128,10 +139,12 @@ public SecurityExchangeHours(
128139
IEnumerable<DateTime> holidayDates,
129140
Dictionary<DayOfWeek, LocalMarketHours> marketHoursForEachDayOfWeek,
130141
IReadOnlyDictionary<DateTime, TimeSpan> earlyCloses,
131-
IReadOnlyDictionary<DateTime, TimeSpan> lateOpens)
142+
IReadOnlyDictionary<DateTime, TimeSpan> lateOpens,
143+
IEnumerable<DateTime> bankHolidayDates = null)
132144
{
133145
TimeZone = timeZone;
134146
_holidays = holidayDates.Select(x => x.Date.Ticks).ToHashSet();
147+
_bankHolidays = (bankHolidayDates ?? Enumerable.Empty<DateTime>()).Select(x => x.Date.Ticks).ToHashSet();
135148
_earlyCloses = earlyCloses;
136149
_lateOpens = lateOpens;
137150
_openHoursByDay = marketHoursForEachDayOfWeek;

Common/Util/MarketHoursDatabaseJsonConverter.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,12 @@ public class MarketHoursDatabaseEntryJson
237237
[JsonProperty("lateOpens")]
238238
public Dictionary<string, TimeSpan> LateOpens { get; set; } = new Dictionary<string, TimeSpan>();
239239

240+
/// <summary>
241+
/// Bank holidays date strings
242+
/// </summary>
243+
[JsonProperty("bankHolidays")]
244+
public List<string> BankHolidays { get; set; } = new();
245+
240246
/// <summary>
241247
/// Initializes a new instance of the <see cref="MarketHoursDatabaseEntryJson"/> class
242248
/// </summary>
@@ -283,6 +289,7 @@ public MarketHoursDatabase.Entry Convert(MarketHoursDatabase.Entry underlyingEnt
283289
{ DayOfWeek.Saturday, new LocalMarketHours(DayOfWeek.Saturday, Saturday) }
284290
};
285291
var holidayDates = Holidays.Select(x => DateTime.ParseExact(x, "M/d/yyyy", CultureInfo.InvariantCulture)).ToHashSet();
292+
var bankHolidayDates = BankHolidays.Select(x => DateTime.ParseExact(x, "M/d/yyyy", CultureInfo.InvariantCulture)).ToHashSet();
286293
IReadOnlyDictionary<DateTime, TimeSpan> earlyCloses = EarlyCloses.ToDictionary(x => DateTime.ParseExact(x.Key, "M/d/yyyy", CultureInfo.InvariantCulture), x => x.Value);
287294
IReadOnlyDictionary<DateTime, TimeSpan> lateOpens = LateOpens.ToDictionary(x => DateTime.ParseExact(x.Key, "M/d/yyyy", CultureInfo.InvariantCulture), x => x.Value);
288295

@@ -293,6 +300,10 @@ public MarketHoursDatabase.Entry Convert(MarketHoursDatabase.Entry underlyingEnt
293300
{
294301
holidayDates = underlyingEntry.ExchangeHours.Holidays;
295302
}
303+
if (bankHolidayDates.Count == 0)
304+
{
305+
bankHolidayDates = underlyingEntry.ExchangeHours.BankHolidays;
306+
}
296307
if (earlyCloses.Count == 0)
297308
{
298309
earlyCloses = underlyingEntry.ExchangeHours.EarlyCloses;
@@ -310,6 +321,11 @@ public MarketHoursDatabase.Entry Convert(MarketHoursDatabase.Entry underlyingEnt
310321
holidayDates.UnionWith(marketEntry.ExchangeHours.Holidays);
311322
}
312323

324+
if (marketEntry.ExchangeHours.BankHolidays.Count > 0)
325+
{
326+
bankHolidayDates.UnionWith(marketEntry.ExchangeHours.BankHolidays);
327+
}
328+
313329
if (marketEntry.ExchangeHours.EarlyCloses.Count > 0 )
314330
{
315331
earlyCloses = MergeLateOpensAndEarlyCloses(marketEntry.ExchangeHours.EarlyCloses, earlyCloses);
@@ -321,7 +337,7 @@ public MarketHoursDatabase.Entry Convert(MarketHoursDatabase.Entry underlyingEnt
321337
}
322338
}
323339

324-
var exchangeHours = new SecurityExchangeHours(DateTimeZoneProviders.Tzdb[ExchangeTimeZone], holidayDates, hours, earlyCloses, lateOpens);
340+
var exchangeHours = new SecurityExchangeHours(DateTimeZoneProviders.Tzdb[ExchangeTimeZone], holidayDates, hours, earlyCloses, lateOpens, bankHolidayDates);
325341
return new MarketHoursDatabase.Entry(DateTimeZoneProviders.Tzdb[DataTimeZone], exchangeHours);
326342
}
327343

0 commit comments

Comments
 (0)