spb/hfmarketdata
Public
Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)
JavaScript 53.7%
Python 38.3%
CSS 4.6%
TypeScript 3.1%
1"""Unit tests — holiday calendar and every expiry rule against real exchange dates.23Reference dates were cross-checked with the last daily bar of each contract in the production lake4(FirstRate Data) and the exchange rulebooks (CME Group, ICE, Eurex, Cboe).5"""6from __future__ import annotations78from datetime import date910import pytest11from futures import calendar_us as cal12from futures import expiry13from futures.specs import SPEC_BY_ROOT, SPECS14from futures.symbols import parse_symbol1516# ---- calendar -------------------------------------------------------------------------------------------1718def test_us_holidays_2024():19 assert sorted(cal.us_holidays(2024)) == [date(2024, 1, 1), date(2024, 1, 15), date(2024, 2, 19), date(2024, 3, 29),20 date(2024, 5, 27), date(2024, 6, 19), date(2024, 7, 4), date(2024, 9, 2),21 date(2024, 11, 28), date(2024, 12, 25)]222324def test_observed_rules():25 assert date(2021, 12, 24) in cal.us_holidays(2021) # Christmas 2021 on Saturday → Friday26 assert date(2021, 12, 31) not in cal.us_holidays(2021) # New Year 2022 on Saturday → no observance27 assert date(2022, 1, 1) not in cal.us_holidays(2022)28 assert date(2023, 1, 2) in cal.us_holidays(2023) # New Year 2023 on Sunday → Monday29 assert date(2022, 6, 20) in cal.us_holidays(2022) # Juneteenth 2022 on Sunday → Monday30 assert date(2021, 6, 18) not in cal.us_holidays(2021) # Juneteenth not observed before 202231 assert date(2026, 7, 3) in cal.us_holidays(2026) # July 4 2026 on Saturday → Friday323334def test_easter_and_eurex():35 assert cal.easter(2024) == date(2024, 3, 31) and cal.easter(2025) == date(2025, 4, 20) and cal.easter(2030) == date(2030, 4, 21)36 hs = cal.eurex_holidays(2025)37 assert {date(2025, 4, 18), date(2025, 4, 21), date(2025, 5, 1), date(2025, 12, 24), date(2025, 12, 26), date(2025, 12, 31)} <= hs383940def test_business_day_helpers():41 assert not cal.is_business_day(date(2024, 12, 25))42 assert not cal.is_business_day(date(2024, 12, 28))43 assert cal.add_business_days(date(2024, 12, 24), 1) == date(2024, 12, 26)44 assert cal.add_business_days(date(2024, 12, 31), -7) == date(2024, 12, 19)45 assert cal.last_business_day_of_month(2024, 11) == date(2024, 11, 29)46 assert cal.first_business_day_of_month(2025, 1) == date(2025, 1, 2)47 assert cal.nth_business_day_of_month(2024, 12, 10) == date(2024, 12, 13)48 assert cal.business_days_between(date(2024, 12, 20), date(2024, 12, 27)) == 3 # 23, 24, 2649 assert cal.previous_business_day(date(2024, 12, 15)) == date(2024, 12, 13)50 assert cal.next_business_day(date(2024, 12, 25)) == date(2024, 12, 26)51 assert cal.shift_month(2024, 1, -1) == (2023, 12) and cal.shift_month(2024, 12, 1) == (2025, 1)525354# ---- rules vs real dates ----------------------------------------------------------------------------------5556REAL = [57 # symbol, rule, last trading date, justification58 ("ESZ24", "third_friday", "2024-12-20", "3rd Friday of December 2024"),59 ("ESM24", "third_friday", "2024-06-21", "3rd Friday of June 2024"),60 ("FDAXZ24", "third_friday_eurex", "2024-12-20", "3rd Friday (Eurex calendar)"),61 ("CLZ24", "cl_rule", "2024-11-20", "25 Nov 2024 (Mon) is a business day → 3 business days before = Wed 20 Nov"),62 ("CLF25", "cl_rule", "2024-12-19", "25 Dec is a holiday → business day before = 24 Dec → 3 business days before = 19 Dec"),63 ("CLG25", "cl_rule", "2025-01-21", "25 Jan 2025 (Sat) → Fri 24 Jan → 3 business days before (MLK 20 Jan excluded) = Tue 21 Jan"),64 ("MCLZ24", "cl_minus_1", "2024-11-19", "one business day before CLZ24"),65 ("NGZ24", "ng_rule", "2024-11-26", "3 business days before 1 Dec 2024 (Thanksgiving 28 Nov excluded)"),66 ("NGF25", "ng_rule", "2024-12-27", "3 business days before 1 Jan 2025"),67 ("GCZ24", "metals_rule", "2024-12-27", "3rd last business day of December 2024 (31, 30, 27)"),68 ("GCG25", "metals_rule", "2025-02-26", "3rd last business day of February 2025"),69 ("PLF25", "metals_rule", "2025-01-29", "3rd last business day of January 2025"),70 ("ZNZ24", "treasury_rule", "2024-12-19", "7th business day before the last business day (31 Dec) — 25 Dec excluded"),71 ("ZNH25", "treasury_rule", "2025-03-20", "7th business day before 31 Mar 2025"),72 ("USZ24", "treasury_rule", "2024-12-19", "same rule for the 30-year bond (lake root US = ZB)"),73 ("ZTZ24", "last_business_day", "2024-12-31", "2-year note: last business day of the contract month"),74 ("ZCZ24", "grains_rule", "2024-12-13", "business day prior to 15 Dec 2024 (Sunday) → Fri 13 Dec"),75 ("ZSX24", "grains_rule", "2024-11-14", "business day prior to Fri 15 Nov 2024"),76 ("E6Z24", "fx_rule", "2024-12-16", "2 business days before the 3rd Wednesday (18 Dec 2024)"),77 ("E6H25", "fx_rule", "2025-03-17", "2 business days before Wed 19 Mar 2025"),78 ("DXZ24", "fx_rule", "2024-12-16", "ICE dollar index follows the same rule"),79 ("VXZ24", "vx_rule", "2024-12-18", "Wednesday 30 days before the 3rd Friday of January 2025 (17 Jan)"),80 ("VXF25", "vx_rule", "2025-01-22", "30 days before Fri 21 Feb 2025"),81 ("VXH25", "vx_rule", "2025-03-18", "3rd Friday of April 2025 is Good Friday → SPX expires Thu 17 Apr → 30 days before = Tue 18 Mar"),82 ("BTCZ24", "last_friday", "2024-12-27", "last Friday of December 2024"),83 ("HOZ24", "prior_month_last_business_day", "2024-11-29", "last business day of November 2024"),84 ("SBH25", "prior_month_last_business_day", "2025-02-28", "Sugar #11: last business day of the month preceding"),85 ("BZZ24", "bz_rule", "2024-10-31", "last business day of the 2nd month preceding December"),86 ("LEZ24", "last_business_day", "2024-12-31", "Live cattle: last business day of the contract month"),87 ("HEZ24", "he_rule", "2024-12-13", "Lean hogs: 10th business day of December 2024"),88 ("GFX24", "gf_rule", "2024-11-21", "Feeder cattle: last Thursday is Thanksgiving → Thursday before"),89 ("GFQ24", "gf_rule", "2024-08-29", "Feeder cattle: last Thursday of August 2024"),90 ("KCZ24", "kc_rule", "2024-12-18", "Coffee: 8 business days before 31 Dec 2024 (25 Dec excluded)"),91 ("CCZ24", "cc_rule", "2024-12-13", "Cocoa: 11 business days before 31 Dec 2024"),92 ("CTZ24", "ct_rule", "2024-12-06", "Cotton: 17 business days from the end of the spot month"),93 ("OJF25", "oj_rule", "2025-01-10", "FCOJ: 14th business day before 31 Jan 2025 (MLK excluded)"),94 ("FGBLZ24", "bund_rule", "2024-12-06", "Bund: delivery 10 Dec 2024 → 2 exchange days before"),95 ("SR3Z24", "sr3_rule", "2025-03-18", "3-month SOFR: business day before the 3rd Wednesday of March 2025"),96 ("NKDZ24", "second_friday_minus_1", "2024-12-12", "Nikkei: business day before the 2nd Friday (13 Dec 2024)"),97]9899100@pytest.mark.parametrize("symbol, rule, expected, why", REAL, ids=[r[0] for r in REAL])101def test_rules_against_real_dates(symbol, rule, expected, why):102 cs = parse_symbol(symbol)103 assert expiry.compute(rule, cs.year, cs.month) == date.fromisoformat(expected), why104105106def test_spec_rule_matches_expected_family():107 """The reference table must point each verified root at the verified rule."""108 for symbol, rule, _, _ in REAL:109 root = parse_symbol(symbol).root110 assert SPEC_BY_ROOT[root].expiry_rule == rule, root111112113def test_first_notice_rules():114 assert expiry.compute_first_notice("prior_month_last_business_day", 2024, 12) == date(2024, 11, 29) # ZN/GC/ZC Z24115 assert expiry.compute_first_notice("cl", 2024, 12) == date(2024, 11, 21) # CLZ24: day after LTD116 assert expiry.compute_first_notice("kc", 2024, 12) == date(2024, 11, 20) # 7 business days before 2 Dec117 assert expiry.compute_first_notice(None, 2024, 12) is None118119120def test_unknown_rule_returns_none():121 assert expiry.compute("data", 2024, 12) is None122 assert expiry.compute(None, 2024, 12) is None123 assert expiry.compute("nope", 2024, 12) is None124125126def test_every_spec_rule_exists():127 for s in SPECS:128 assert s.expiry_rule == "data" or s.expiry_rule in expiry.RULES, s.root129 assert s.first_notice_rule is None or s.first_notice_rule in expiry.FIRST_NOTICE_RULES, s.root130 assert s.calendar in ("us", "eurex")131 assert len(s.rth) == 2132 assert len({s.root for s in SPECS}) == len(SPECS)133