SPB Git forge

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)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%

futures: symboles de contrats (alias CME↔FirstRate), calendrier jours fériés US/Eurex, règles d'échéance et référentiel des racines

- symbols.py : parse ESZ25/ESZ2025/ES_Z25/6EZ25 → (racine lac, code mois, année) ; table ROOT_ALIASES (6E→E6, 6J→J1, ZB→US…) ; 400 INVALID_CONTRACT_SYMBOL
- calendar_us.py : jours fériés CME/NYSE (règles observées, Good Friday, Juneteenth ≥ 2022) + Eurex, arithmétique jours ouvrés
- expiry.py : 24 règles (third_friday, cl_rule, ng_rule, metals_rule, treasury_rule, grains_rule, fx_rule, vx_rule, bund_rule, sr3_rule…) + premiers avis, vérifiées sur les dernières barres réelles du lac
- specs.py : ~120 racines (taille, tick, règlement, cycle, règle, RTH ET) ; valeurs incertaines = null, racines inconnues = derived

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 20 days ago (Sep 4, 2026) parent 7ee2ef9

5 changed files +790 −0

added hfmarketdata/api/futures/__init__.py +4 −0
@@ -0,0 +1,4 @@
1 +"""Individual futures contracts, chains, continuous series and term structure (chantier 1).
2 +
3 +Author: Simon-Pierre Boucher <contact@spboucher.ai>
4 +"""
added hfmarketdata/api/futures/calendar_us.py +149 −0
@@ -0,0 +1,149 @@
1 +"""Small exchange holiday calendars + business-day arithmetic (2000–2035).
2 +
3 +`us` = CME Group / NYSE common holidays (fixed + observed rules, Good Friday, Juneteenth from 2022).
4 +`eurex` = Eurex trading holidays (New Year, Good Friday, Easter Monday, 1 May, 24–26 Dec, 31 Dec).
5 +No Saturday/Sunday business days. Weather/state-funeral one-off NYSE closures are NOT included
6 +because CME futures kept trading on those days.
7 +
8 +Author: Simon-Pierre Boucher <contact@spboucher.ai>
9 +"""
10 +from __future__ import annotations
11 +
12 +from datetime import date, timedelta
13 +from functools import cache
14 +
15 +Calendar = str # "us" | "eurex"
16 +
17 +
18 +def easter(year: int) -> date:
19 + """Gregorian Easter Sunday (Anonymous/Meeus algorithm)."""
20 + a = year % 19
21 + b, c = divmod(year, 100)
22 + d, e = divmod(b, 4)
23 + f = (b + 8) // 25
24 + g = (b - f + 1) // 3
25 + h = (19 * a + b - d - g + 15) % 30
26 + i, k = divmod(c, 4)
27 + l = (32 + 2 * e + 2 * i - h - k) % 7
28 + m = (a + 11 * h + 22 * l) // 451
29 + month = (h + l - 7 * m + 114) // 31
30 + day = (h + l - 7 * m + 114) % 31 + 1
31 + return date(year, month, day)
32 +
33 +
34 +def nth_weekday(year: int, month: int, weekday: int, n: int) -> date:
35 + """n-th (1-based) given weekday (Mon=0) of a month; n=-1 → last."""
36 + if n > 0:
37 + first = date(year, month, 1)
38 + off = (weekday - first.weekday()) % 7
39 + return first + timedelta(days=off + 7 * (n - 1))
40 + last = date(year + (month == 12), (month % 12) + 1, 1) - timedelta(days=1)
41 + off = (last.weekday() - weekday) % 7
42 + return last - timedelta(days=off)
43 +
44 +
45 +def _observed(d: date) -> date | None:
46 + """Saturday → Friday, Sunday → Monday (US federal observance). None if Saturday → no observance
47 + is used for New Year's Day (NYSE/CME rule: if Jan 1 is a Saturday there is no holiday on Dec 31)."""
48 + if d.weekday() == 5:
49 + return d - timedelta(days=1)
50 + if d.weekday() == 6:
51 + return d + timedelta(days=1)
52 + return d
53 +
54 +
55 +@cache
56 +def us_holidays(year: int) -> frozenset[date]:
57 + hs: set[date] = set()
58 + ny = date(year, 1, 1)
59 + if ny.weekday() == 6:
60 + hs.add(ny + timedelta(days=1))
61 + elif ny.weekday() != 5:
62 + hs.add(ny)
63 + hs.add(nth_weekday(year, 1, 0, 3)) # Martin Luther King Jr. Day
64 + hs.add(nth_weekday(year, 2, 0, 3)) # Presidents' Day
65 + hs.add(easter(year) - timedelta(days=2)) # Good Friday
66 + hs.add(nth_weekday(year, 5, 0, -1)) # Memorial Day
67 + if year >= 2022:
68 + hs.add(_observed(date(year, 6, 19))) # Juneteenth
69 + hs.add(_observed(date(year, 7, 4))) # Independence Day
70 + hs.add(nth_weekday(year, 9, 0, 1)) # Labor Day
71 + hs.add(nth_weekday(year, 11, 3, 4)) # Thanksgiving
72 + hs.add(_observed(date(year, 12, 25))) # Christmas
73 + return frozenset(h for h in hs if h is not None)
74 +
75 +
76 +@cache
77 +def eurex_holidays(year: int) -> frozenset[date]:
78 + e = easter(year)
79 + hs = {date(year, 1, 1), e - timedelta(days=2), e + timedelta(days=1), date(year, 5, 1),
80 + date(year, 12, 24), date(year, 12, 25), date(year, 12, 26), date(year, 12, 31)}
81 + return frozenset(hs)
82 +
83 +
84 +def holidays(year: int, cal: Calendar = "us") -> frozenset[date]:
85 + return eurex_holidays(year) if cal == "eurex" else us_holidays(year)
86 +
87 +
88 +def is_business_day(d: date, cal: Calendar = "us") -> bool:
89 + return d.weekday() < 5 and d not in holidays(d.year, cal)
90 +
91 +
92 +def add_business_days(d: date, n: int, cal: Calendar = "us") -> date:
93 + """Move n business days from d (n may be negative). d itself does not count."""
94 + step = 1 if n >= 0 else -1
95 + remaining = abs(n)
96 + while remaining:
97 + d += timedelta(days=step)
98 + if is_business_day(d, cal):
99 + remaining -= 1
100 + return d
101 +
102 +
103 +def previous_business_day(d: date, cal: Calendar = "us", inclusive: bool = True) -> date:
104 + """Latest business day ≤ d (inclusive) or < d."""
105 + if not inclusive:
106 + d -= timedelta(days=1)
107 + while not is_business_day(d, cal):
108 + d -= timedelta(days=1)
109 + return d
110 +
111 +
112 +def next_business_day(d: date, cal: Calendar = "us", inclusive: bool = True) -> date:
113 + if not inclusive:
114 + d += timedelta(days=1)
115 + while not is_business_day(d, cal):
116 + d += timedelta(days=1)
117 + return d
118 +
119 +
120 +def last_business_day_of_month(year: int, month: int, cal: Calendar = "us") -> date:
121 + last = date(year + (month == 12), (month % 12) + 1, 1) - timedelta(days=1)
122 + return previous_business_day(last, cal)
123 +
124 +
125 +def first_business_day_of_month(year: int, month: int, cal: Calendar = "us") -> date:
126 + return next_business_day(date(year, month, 1), cal)
127 +
128 +
129 +def nth_business_day_of_month(year: int, month: int, n: int, cal: Calendar = "us") -> date:
130 + d = first_business_day_of_month(year, month, cal)
131 + return add_business_days(d, n - 1, cal)
132 +
133 +
134 +def business_days_between(start: date, end: date, cal: Calendar = "us") -> int:
135 + """Number of business days strictly between start and end (start < end)."""
136 + if end <= start:
137 + return 0
138 + n = 0
139 + d = start + timedelta(days=1)
140 + while d < end:
141 + if is_business_day(d, cal):
142 + n += 1
143 + d += timedelta(days=1)
144 + return n
145 +
146 +
147 +def shift_month(year: int, month: int, delta: int) -> tuple[int, int]:
148 + m = month - 1 + delta
149 + return year + m // 12, m % 12 + 1
added hfmarketdata/api/futures/expiry.py +242 −0
@@ -0,0 +1,242 @@
1 +"""Expiry (last trading day) and first-notice rules per product family.
2 +
3 +Every rule is a pure function `(year, month) -> date` of the *contract* month, using the exchange
4 +calendar of `calendar_us`. `compute(rule, year, month)` dispatches on the rule key stored in
5 +`specs.py`; unknown/`data` rules return None so the caller falls back to the last data date
6 +(`expiration_source="data"`). Rules are verified against real last-bar dates in the lake, e.g.
7 +ESZ24 2024-12-20, CLZ24 2024-11-20, GCZ24 2024-12-27, ZNZ24 2024-12-19, NGZ24 2024-11-26,
8 +E6Z24 2024-12-16, VXZ24 2024-12-18, ZCZ24 2024-12-13, KCZ24 2024-12-18, FGBLZ24 2024-12-06.
9 +
10 +Author: Simon-Pierre Boucher <contact@spboucher.ai>
11 +"""
12 +from __future__ import annotations
13 +
14 +from collections.abc import Callable
15 +from datetime import date, timedelta
16 +
17 +from . import calendar_us as cal
18 +from .calendar_us import (
19 + add_business_days,
20 + first_business_day_of_month,
21 + is_business_day,
22 + last_business_day_of_month,
23 + next_business_day,
24 + nth_business_day_of_month,
25 + nth_weekday,
26 + previous_business_day,
27 + shift_month,
28 +)
29 +
30 +Rule = Callable[[int, int], date]
31 +
32 +
33 +def third_friday(y: int, m: int) -> date:
34 + """Equity index (ES, NQ, YM, RTY, FDAX, FESX…): 3rd Friday of the contract month; if it is a holiday,
35 + the preceding business day."""
36 + return previous_business_day(nth_weekday(y, m, 4, 3))
37 +
38 +
39 +def third_friday_eurex(y: int, m: int) -> date:
40 + return previous_business_day(nth_weekday(y, m, 4, 3), "eurex")
41 +
42 +
43 +def second_friday_minus_1(y: int, m: int) -> date:
44 + """Nikkei 225 (NKD/NIY): business day preceding the 2nd Friday of the contract month."""
45 + return previous_business_day(nth_weekday(y, m, 4, 2), inclusive=False)
46 +
47 +
48 +def cl_rule(y: int, m: int) -> date:
49 + """WTI (CL): trading terminates 3 business days before the 25th calendar day of the month preceding
50 + the contract month; if the 25th is not a business day, 3 business days before the business day
51 + preceding the 25th."""
52 + py, pm = shift_month(y, m, -1)
53 + anchor = previous_business_day(date(py, pm, 25))
54 + return add_business_days(anchor, -3)
55 +
56 +
57 +def cl_minus_1(y: int, m: int) -> date:
58 + """Micro WTI (MCL): one business day before the CL termination."""
59 + return add_business_days(cl_rule(y, m), -1)
60 +
61 +
62 +def ng_rule(y: int, m: int) -> date:
63 + """Henry Hub (NG, HH): 3 business days before the first calendar day of the contract month."""
64 + return add_business_days(date(y, m, 1), -3)
65 +
66 +
67 +def ng_minus_1(y: int, m: int) -> date:
68 + """E-mini Natural Gas (QG): one business day before the NG termination (4th last business day of the
69 + month preceding the contract month)."""
70 + return add_business_days(ng_rule(y, m), -1)
71 +
72 +
73 +def prior_month_last_business_day(y: int, m: int) -> date:
74 + """HO, RB, SB (Sugar #11), BR: last business day of the month preceding the contract month."""
75 + py, pm = shift_month(y, m, -1)
76 + return last_business_day_of_month(py, pm)
77 +
78 +
79 +def bz_rule(y: int, m: int) -> date:
80 + """Brent (BZ, ICE B): last business day of the 2nd month preceding the contract month."""
81 + py, pm = shift_month(y, m, -2)
82 + return last_business_day_of_month(py, pm)
83 +
84 +
85 +def metals_rule(y: int, m: int) -> date:
86 + """COMEX/NYMEX metals (GC, SI, HG, PL, PA, MGC, SIL, ALI): 3rd last business day of the contract month."""
87 + return add_business_days(last_business_day_of_month(y, m), -2)
88 +
89 +
90 +def last_business_day(y: int, m: int) -> date:
91 + """LE, ZT, ZF, ZQ, SR1, HRC: last business day of the contract month."""
92 + return last_business_day_of_month(y, m)
93 +
94 +
95 +def treasury_rule(y: int, m: int) -> date:
96 + """ZN, ZB(US), UB, TN: 7th business day preceding the last business day of the contract month."""
97 + return add_business_days(last_business_day_of_month(y, m), -7)
98 +
99 +
100 +def grains_rule(y: int, m: int) -> date:
101 + """CBOT grains (ZC, ZS, ZW, KE, ZM, ZL, ZO, ZR, XC) and ICE canola (RS): business day prior to the
102 + 15th calendar day of the contract month."""
103 + return previous_business_day(date(y, m, 15), inclusive=False)
104 +
105 +
106 +def fx_rule(y: int, m: int) -> date:
107 + """CME FX (E6, J1, B6, A6, AD, E1, N6, MP, T6, E7, J7, crosses) and ICE DX: 2nd business day before
108 + the 3rd Wednesday of the contract month."""
109 + return add_business_days(nth_weekday(y, m, 2, 3), -2)
110 +
111 +
112 +def vx_rule(y: int, m: int) -> date:
113 + """VIX futures (VX, VXM) and VSTOXX (FVSA): the Wednesday 30 calendar days before the 3rd Friday of
114 + the following month (the SPX option expiry). If that Friday is a holiday the option expires the
115 + preceding Thursday and the future 30 days before it; if the resulting Wednesday is a holiday,
116 + the preceding business day."""
117 + ny, nm = shift_month(y, m, 1)
118 + spx = nth_weekday(ny, nm, 4, 3)
119 + if not is_business_day(spx):
120 + spx = previous_business_day(spx, inclusive=False)
121 + d = spx - timedelta(days=30)
122 + return previous_business_day(d)
123 +
124 +
125 +def last_friday(y: int, m: int) -> date:
126 + """CME crypto (BTC, MBT, MET): last Friday of the contract month (preceding business day if holiday)."""
127 + return previous_business_day(nth_weekday(y, m, 4, -1))
128 +
129 +
130 +def he_rule(y: int, m: int) -> date:
131 + """Lean Hogs (HE) and Pork Cutout (PRK): 10th business day of the contract month."""
132 + return nth_business_day_of_month(y, m, 10)
133 +
134 +
135 +def gf_rule(y: int, m: int) -> date:
136 + """Feeder Cattle (GF): last Thursday of the contract month; November → the Thursday before
137 + Thanksgiving; holiday → preceding business day."""
138 + d = nth_weekday(y, m, 3, -1)
139 + if m == 11 and d == nth_weekday(y, 11, 3, 4):
140 + d -= timedelta(days=7)
141 + return previous_business_day(d)
142 +
143 +
144 +def kc_rule(y: int, m: int) -> date:
145 + """Coffee C (KC): 8 business days prior to the last business day of the contract month."""
146 + return add_business_days(last_business_day_of_month(y, m), -8)
147 +
148 +
149 +def cc_rule(y: int, m: int) -> date:
150 + """Cocoa (CC, London C): 11 business days prior to the last business day of the contract month."""
151 + return add_business_days(last_business_day_of_month(y, m), -11)
152 +
153 +
154 +def ct_rule(y: int, m: int) -> date:
155 + """Cotton No. 2 (CT): 17 business days from the end of the spot month (the last business day counts
156 + as day 1) = 16 business days before the last business day."""
157 + return add_business_days(last_business_day_of_month(y, m), -16)
158 +
159 +
160 +def oj_rule(y: int, m: int) -> date:
161 + """FCOJ-A (OJ): 14th business day prior to the last business day of the contract month."""
162 + return add_business_days(last_business_day_of_month(y, m), -14)
163 +
164 +
165 +def bund_rule(y: int, m: int) -> date:
166 + """Eurex fixed income (FGBL, FGBM, FGBS, FGBX, FOAT, FBTP, FBTS, FBON): delivery day = 10th calendar
167 + day of the contract month (next exchange day if not one); last trading day = 2 exchange days
168 + before the delivery day."""
169 + delivery = next_business_day(date(y, m, 10), "eurex")
170 + return add_business_days(delivery, -2, "eurex")
171 +
172 +
173 +def sr3_rule(y: int, m: int) -> date:
174 + """3-Month SOFR (SR3): business day before the 3rd Wednesday of the 3rd month after the contract month."""
175 + ny, nm = shift_month(y, m, 3)
176 + return previous_business_day(nth_weekday(ny, nm, 2, 3), inclusive=False)
177 +
178 +
179 +# ---- first notice ------------------------------------------------------------------------------------
180 +
181 +def fnd_prior_month_last_business_day(y: int, m: int) -> date:
182 + """Treasuries, grains, metals: last business day of the month preceding the contract month."""
183 + py, pm = shift_month(y, m, -1)
184 + return last_business_day_of_month(py, pm)
185 +
186 +
187 +def fnd_kc(y: int, m: int) -> date:
188 + """Coffee C: 7 business days prior to the first business day of the contract month."""
189 + return add_business_days(first_business_day_of_month(y, m), -7)
190 +
191 +
192 +def fnd_cl(y: int, m: int) -> date:
193 + """WTI: first business day after the last trading day."""
194 + return add_business_days(cl_rule(y, m), 1)
195 +
196 +
197 +RULES: dict[str, Rule] = {
198 + "third_friday": third_friday,
199 + "third_friday_eurex": third_friday_eurex,
200 + "second_friday_minus_1": second_friday_minus_1,
201 + "cl_rule": cl_rule,
202 + "cl_minus_1": cl_minus_1,
203 + "ng_rule": ng_rule,
204 + "ng_minus_1": ng_minus_1,
205 + "prior_month_last_business_day": prior_month_last_business_day,
206 + "bz_rule": bz_rule,
207 + "metals_rule": metals_rule,
208 + "last_business_day": last_business_day,
209 + "treasury_rule": treasury_rule,
210 + "grains_rule": grains_rule,
211 + "fx_rule": fx_rule,
212 + "vx_rule": vx_rule,
213 + "last_friday": last_friday,
214 + "he_rule": he_rule,
215 + "gf_rule": gf_rule,
216 + "kc_rule": kc_rule,
217 + "cc_rule": cc_rule,
218 + "ct_rule": ct_rule,
219 + "oj_rule": oj_rule,
220 + "bund_rule": bund_rule,
221 + "sr3_rule": sr3_rule,
222 +}
223 +
224 +FIRST_NOTICE_RULES: dict[str, Rule] = {
225 + "prior_month_last_business_day": fnd_prior_month_last_business_day,
226 + "kc": fnd_kc,
227 + "cl": fnd_cl,
228 +}
229 +
230 +
231 +def compute(rule: str | None, year: int, month: int) -> date | None:
232 + """Last trading date for a contract month, or None when the rule is `data`/unknown."""
233 + fn = RULES.get(rule or "")
234 + return fn(year, month) if fn else None
235 +
236 +
237 +def compute_first_notice(rule: str | None, year: int, month: int) -> date | None:
238 + fn = FIRST_NOTICE_RULES.get(rule or "")
239 + return fn(year, month) if fn else None
240 +
241 +
242 +__all__ = ["FIRST_NOTICE_RULES", "RULES", "cal", "compute", "compute_first_notice"]
added hfmarketdata/api/futures/specs.py +277 −0
@@ -0,0 +1,277 @@
1 +"""Static reference table of futures roots (lake / FirstRate root codes).
2 +
3 +Values come from the exchange contract specifications (CME Group, ICE, Eurex, Euronext, Cboe).
4 +Fields left `None` are *not* known with certainty and stay null — the API never invents values.
5 +Roots present in the lake but absent here get a `derived` entry (name from meta/futures/futures.csv,
6 +`expiry_rule="data"`).
7 +
8 +RTH windows are US/Eastern clock times used by the `session=rth|eth` filter; default 09:30–16:00.
9 +
10 +Author: Simon-Pierre Boucher <contact@spboucher.ai>
11 +"""
12 +from __future__ import annotations
13 +
14 +from dataclasses import asdict, dataclass, field
15 +
16 +from .symbols import ALIASES_OF_ROOT
17 +
18 +ALL_MONTHS = "FGHJKMNQUVXZ"
19 +QUARTERLY = "HMUZ"
20 +DEFAULT_RTH = ("09:30", "16:00")
21 +
22 +
23 +@dataclass(frozen=True)
24 +class RootSpec:
25 + root: str
26 + name: str | None
27 + exchange: str | None
28 + asset_class: str | None
29 + currency: str | None = None
30 + contract_size: float | None = None
31 + contract_size_unit: str | None = None
32 + tick_size: float | None = None
33 + tick_value: float | None = None
34 + settlement_type: str | None = None # cash | physical
35 + month_cycle: str | None = None
36 + expiry_rule: str = "data"
37 + first_notice_rule: str | None = None # key in expiry.FIRST_NOTICE_RULES
38 + calendar: str = "us" # us | eurex (business-day calendar used by the rules)
39 + rth: tuple[str, str] = DEFAULT_RTH # US/Eastern
40 + source: str = "reference"
41 + aliases: list[str] = field(default_factory=list)
42 +
43 + def as_dict(self) -> dict:
44 + d = asdict(self)
45 + d["rth_start"], d["rth_end"] = self.rth
46 + del d["rth"]
47 + return d
48 +
49 +
50 +def _s(root: str, name: str, exchange: str, asset_class: str, currency: str = "USD", **kw) -> RootSpec:
51 + kw.setdefault("aliases", ALIASES_OF_ROOT.get(root, []))
52 + return RootSpec(root=root, name=name, exchange=exchange, asset_class=asset_class, currency=currency, **kw)
53 +
54 +
55 +_EQ = dict(asset_class="equity_index", settlement_type="cash", month_cycle=QUARTERLY, expiry_rule="third_friday")
56 +_EQX = {**_EQ, "expiry_rule": "third_friday_eurex", "calendar": "eurex", "rth": ("03:00", "11:30")}
57 +_EN = dict(asset_class="energy", month_cycle=ALL_MONTHS, rth=("09:00", "14:30"))
58 +_MT = dict(asset_class="metals", settlement_type="physical", expiry_rule="metals_rule",
59 + first_notice_rule="prior_month_last_business_day", rth=("08:20", "13:30"))
60 +_TR = dict(asset_class="rates", settlement_type="physical", month_cycle=QUARTERLY, expiry_rule="treasury_rule",
61 + first_notice_rule="prior_month_last_business_day", rth=("08:20", "15:00"))
62 +_GR = dict(asset_class="ags", settlement_type="physical", expiry_rule="grains_rule",
63 + first_notice_rule="prior_month_last_business_day", rth=("09:30", "14:20"))
64 +_FX = dict(asset_class="fx", settlement_type="physical", month_cycle=QUARTERLY, expiry_rule="fx_rule", rth=("08:20", "15:00"))
65 +_BUND = dict(asset_class="rates", settlement_type="physical", month_cycle=QUARTERLY, expiry_rule="bund_rule",
66 + calendar="eurex", rth=("02:00", "16:00"))
67 +
68 +SPECS: list[RootSpec] = [
69 + # ---- equity index (CME / CBOT) --------------------------------------------------------------------
70 + _s("ES", "E-mini S&P 500", "CME", contract_size=50, contract_size_unit="USD x index", tick_size=0.25, tick_value=12.5, **_EQ),
71 + _s("NQ", "E-mini Nasdaq-100", "CME", contract_size=20, contract_size_unit="USD x index", tick_size=0.25, tick_value=5.0, **_EQ),
72 + _s("YM", "E-mini Dow ($5)", "CBOT", contract_size=5, contract_size_unit="USD x index", tick_size=1.0, tick_value=5.0, **_EQ),
73 + _s("RTY", "E-mini Russell 2000", "CME", contract_size=50, contract_size_unit="USD x index", tick_size=0.10, tick_value=5.0, **_EQ),
74 + _s("MES", "Micro E-mini S&P 500", "CME", contract_size=5, contract_size_unit="USD x index", tick_size=0.25, tick_value=1.25, **_EQ),
75 + _s("MNQ", "Micro E-mini Nasdaq-100", "CME", contract_size=2, contract_size_unit="USD x index", tick_size=0.25, tick_value=0.5, **_EQ),
76 + _s("M2K", "Micro E-mini Russell 2000", "CME", contract_size=5, contract_size_unit="USD x index", tick_size=0.10, tick_value=0.5, **_EQ),
77 + _s("EW", "E-mini S&P MidCap 400", "CME", contract_size=100, contract_size_unit="USD x index", tick_size=0.10, tick_value=10.0, **_EQ),
78 + _s("ESG", "E-mini S&P 500 ESG", "CME", contract_size=50, contract_size_unit="USD x index", tick_size=0.25, tick_value=12.5, **_EQ),
79 + _s("XAE", "E-mini Energy Select Sector", "CME", **_EQ),
80 + _s("XAF", "E-mini Financial Select Sector", "CME", **_EQ),
81 + _s("XAI", "E-mini Industrial Select Sector", "CME", **_EQ),
82 + _s("NKD", "Nikkei 225 (USD)", "CME", contract_size=5, contract_size_unit="USD x index", tick_size=5.0, tick_value=25.0,
83 + asset_class="equity_index", settlement_type="cash", month_cycle=QUARTERLY, expiry_rule="second_friday_minus_1"),
84 + _s("NIY", "Nikkei 225 (JPY)", "CME", currency="JPY", contract_size=500, contract_size_unit="JPY x index", tick_size=5.0, tick_value=2500.0,
85 + asset_class="equity_index", settlement_type="cash", month_cycle=QUARTERLY, expiry_rule="second_friday_minus_1"),
86 + _s("GSCI", "S&P GSCI", "CME", contract_size=250, contract_size_unit="USD x index", tick_size=0.05, tick_value=12.5,
87 + asset_class="equity_index", settlement_type="cash", month_cycle=ALL_MONTHS),
88 + _s("MFS", "Mini MSCI EAFE", "ICE US", contract_size=50, contract_size_unit="USD x index", tick_size=0.05, tick_value=2.5, **_EQ),
89 + _s("MME", "Mini MSCI Emerging Markets", "ICE US", contract_size=50, contract_size_unit="USD x index", tick_size=0.05, tick_value=2.5, **_EQ),
90 + # ---- equity index (Europe) ------------------------------------------------------------------------
91 + _s("FDAX", "DAX", "EUREX", currency="EUR", contract_size=25, contract_size_unit="EUR x index", tick_size=1.0, tick_value=25.0, **_EQX),
92 + _s("FDXM", "Mini-DAX", "EUREX", currency="EUR", contract_size=5, contract_size_unit="EUR x index", tick_size=1.0, tick_value=5.0, **_EQX),
93 + _s("FDXS", "Micro-DAX", "EUREX", currency="EUR", contract_size=1, contract_size_unit="EUR x index", tick_size=1.0, tick_value=1.0, **_EQX),
94 + _s("FESX", "Euro Stoxx 50", "EUREX", currency="EUR", contract_size=10, contract_size_unit="EUR x index", tick_size=1.0, tick_value=10.0, **_EQX),
95 + _s("FXXP", "Stoxx Europe 600", "EUREX", currency="EUR", contract_size=50, contract_size_unit="EUR x index", tick_size=0.10, tick_value=5.0, **_EQX),
96 + _s("FSMX", "Mini-MDAX", "EUREX", currency="EUR", **_EQX),
97 + _s("FTDX", "TecDAX", "EUREX", currency="EUR", **_EQX),
98 + _s("FDIV", "DivDAX", "EUREX", currency="EUR", **_EQX),
99 + _s("FMWO", "MSCI World", "EUREX", currency="USD", **_EQX),
100 + _s("MURA", "MSCI China", "EUREX", currency="USD", **_EQX),
101 + _s("ZRPA", "MSCI Europe", "EUREX", currency="EUR", **_EQX),
102 + _s("ZTWA", "MSCI Emerging Markets Asia", "EUREX", currency="USD", **_EQX),
103 + _s("FVSA", "VSTOXX", "EUREX", currency="EUR", contract_size=100, contract_size_unit="EUR x index", tick_size=0.05, tick_value=5.0,
104 + asset_class="volatility", settlement_type="cash", month_cycle=ALL_MONTHS, expiry_rule="vx_rule", calendar="eurex", rth=("03:00", "11:30")),
105 + _s("FCE", "CAC 40", "Euronext", currency="EUR", contract_size=10, contract_size_unit="EUR x index", tick_size=0.5, tick_value=5.0,
106 + **{**_EQX, "month_cycle": ALL_MONTHS}),
107 + _s("MFC", "Mini CAC 40", "Euronext", currency="EUR", contract_size=1, contract_size_unit="EUR x index", tick_size=0.5, tick_value=0.5,
108 + **{**_EQX, "month_cycle": ALL_MONTHS}),
109 + _s("FTI", "AEX", "Euronext", currency="EUR", contract_size=200, contract_size_unit="EUR x index", tick_size=0.05, tick_value=10.0,
110 + **{**_EQX, "month_cycle": ALL_MONTHS}),
111 + _s("MAX", "Mini AEX", "Euronext", currency="EUR", contract_size=20, contract_size_unit="EUR x index", tick_size=0.05, tick_value=1.0,
112 + **{**_EQX, "month_cycle": ALL_MONTHS}),
113 + _s("BFX", "BEL 20", "Euronext", currency="EUR", **{**_EQX, "month_cycle": ALL_MONTHS}),
114 + _s("PSI", "PSI 20", "Euronext", currency="EUR", **{**_EQX, "month_cycle": ALL_MONTHS}),
115 + _s("FTUK", "FTSE 100", "ICE Europe", currency="GBP", contract_size=10, contract_size_unit="GBP x index", tick_size=0.5, tick_value=5.0,
116 + **{**_EQX, "rth": ("03:00", "11:30")}),
117 + # ---- energy (NYMEX / ICE) -------------------------------------------------------------------------
118 + _s("CL", "Crude Oil WTI", "NYMEX", contract_size=1000, contract_size_unit="barrels", tick_size=0.01, tick_value=10.0,
119 + settlement_type="physical", expiry_rule="cl_rule", first_notice_rule="cl", **_EN),
120 + _s("MCL", "Micro WTI Crude Oil", "NYMEX", contract_size=100, contract_size_unit="barrels", tick_size=0.01, tick_value=1.0,
121 + settlement_type="cash", expiry_rule="cl_minus_1", **_EN),
122 + _s("BZ", "Brent Last Day Financial", "NYMEX", contract_size=1000, contract_size_unit="barrels", tick_size=0.01, tick_value=10.0,
123 + settlement_type="cash", expiry_rule="bz_rule", **_EN),
124 + _s("B", "Brent Crude", "ICE Europe", contract_size=1000, contract_size_unit="barrels", tick_size=0.01, tick_value=10.0,
125 + settlement_type="cash", expiry_rule="bz_rule", **{**_EN, "rth": ("03:00", "14:30")}),
126 + _s("NG", "Henry Hub Natural Gas", "NYMEX", contract_size=10000, contract_size_unit="MMBtu", tick_size=0.001, tick_value=10.0,
127 + settlement_type="physical", expiry_rule="ng_rule", **_EN),
128 + _s("QG", "E-mini Natural Gas", "NYMEX", contract_size=2500, contract_size_unit="MMBtu", tick_size=0.005, tick_value=12.5,
129 + settlement_type="cash", expiry_rule="ng_minus_1", **_EN),
130 + _s("HH", "Natural Gas Last Day Financial", "NYMEX", contract_size=10000, contract_size_unit="MMBtu", tick_size=0.001, tick_value=10.0,
131 + settlement_type="cash", expiry_rule="ng_rule", **_EN),
132 + _s("HO", "NY Harbor ULSD", "NYMEX", contract_size=42000, contract_size_unit="gallons", tick_size=0.0001, tick_value=4.2,
133 + settlement_type="physical", expiry_rule="prior_month_last_business_day", **_EN),
134 + _s("RB", "RBOB Gasoline", "NYMEX", contract_size=42000, contract_size_unit="gallons", tick_size=0.0001, tick_value=4.2,
135 + settlement_type="physical", expiry_rule="prior_month_last_business_day", **_EN),
136 + _s("TTF", "Dutch TTF Natural Gas", "CME", currency="EUR", settlement_type="cash", **_EN),
137 + # ---- metals (COMEX / NYMEX) -----------------------------------------------------------------------
138 + _s("GC", "Gold", "COMEX", contract_size=100, contract_size_unit="troy oz", tick_size=0.10, tick_value=10.0, month_cycle="GJMQVZ", **_MT),
139 + _s("SI", "Silver", "COMEX", contract_size=5000, contract_size_unit="troy oz", tick_size=0.005, tick_value=25.0, month_cycle="FHKNUZ", **_MT),
140 + _s("HG", "Copper", "COMEX", contract_size=25000, contract_size_unit="lbs", tick_size=0.0005, tick_value=12.5, month_cycle="HKNUZ", **_MT),
141 + _s("PL", "Platinum", "NYMEX", contract_size=50, contract_size_unit="troy oz", tick_size=0.10, tick_value=5.0, month_cycle="FJNV", **_MT),
142 + _s("PA", "Palladium", "NYMEX", contract_size=100, contract_size_unit="troy oz", tick_size=0.50, tick_value=50.0, month_cycle="HMUZ", **_MT),
143 + _s("MGC", "Micro Gold", "COMEX", contract_size=10, contract_size_unit="troy oz", tick_size=0.10, tick_value=1.0, month_cycle="GJMQVZ", **_MT),
144 + _s("SIL", "Micro Silver", "COMEX", contract_size=1000, contract_size_unit="troy oz", tick_size=0.005, tick_value=5.0, month_cycle="FHKNUZ", **_MT),
145 + _s("ALI", "Aluminum", "COMEX", contract_size=25, contract_size_unit="metric tons", tick_size=0.25, tick_value=6.25, month_cycle=ALL_MONTHS, **_MT),
146 + _s("HRC", "U.S. Midwest Domestic Hot-Rolled Coil Steel", "COMEX", contract_size=20, contract_size_unit="short tons", tick_size=1.0, tick_value=20.0,
147 + asset_class="metals", settlement_type="cash", month_cycle=ALL_MONTHS, expiry_rule="last_business_day"),
148 + # ---- rates (CBOT / CME / Eurex / ICE) -------------------------------------------------------------
149 + _s("ZN", "10-Year T-Note", "CBOT", contract_size=100000, contract_size_unit="USD face value", tick_size=0.015625, tick_value=15.625, **_TR),
150 + _s("US", "30-Year T-Bond", "CBOT", contract_size=100000, contract_size_unit="USD face value", tick_size=0.03125, tick_value=31.25, **_TR),
151 + _s("UB", "Ultra T-Bond", "CBOT", contract_size=100000, contract_size_unit="USD face value", tick_size=0.03125, tick_value=31.25, **_TR),
152 + _s("TN", "Ultra 10-Year T-Note", "CBOT", contract_size=100000, contract_size_unit="USD face value", tick_size=0.015625, tick_value=15.625, **_TR),
153 + _s("ZF", "5-Year T-Note", "CBOT", contract_size=100000, contract_size_unit="USD face value", tick_size=0.0078125, tick_value=7.8125,
154 + **{**_TR, "expiry_rule": "last_business_day"}),
155 + _s("ZT", "2-Year T-Note", "CBOT", contract_size=200000, contract_size_unit="USD face value", tick_size=0.00390625, tick_value=7.8125,
156 + **{**_TR, "expiry_rule": "last_business_day"}),
157 + _s("ZQ", "30-Day Fed Funds", "CBOT", contract_size=5000000, contract_size_unit="USD notional", tick_size=0.005, tick_value=20.835,
158 + asset_class="rates", settlement_type="cash", month_cycle=ALL_MONTHS, expiry_rule="last_business_day", rth=("08:20", "15:00")),
159 + _s("SR3", "3-Month SOFR", "CME", contract_size=2500, contract_size_unit="USD x index", tick_size=0.0025, tick_value=6.25,
160 + asset_class="rates", settlement_type="cash", month_cycle=QUARTERLY, expiry_rule="sr3_rule", rth=("08:20", "15:00")),
161 + _s("SR1", "1-Month SOFR", "CME", contract_size=4167, contract_size_unit="USD x index", tick_size=0.0025, tick_value=10.4175,
162 + asset_class="rates", settlement_type="cash", month_cycle=ALL_MONTHS, expiry_rule="last_business_day", rth=("08:20", "15:00")),
163 + _s("FGBL", "Euro-Bund", "EUREX", currency="EUR", contract_size=100000, contract_size_unit="EUR face value", tick_size=0.01, tick_value=10.0, **_BUND),
164 + _s("FGBM", "Euro-Bobl", "EUREX", currency="EUR", contract_size=100000, contract_size_unit="EUR face value", tick_size=0.01, tick_value=10.0, **_BUND),
165 + _s("FGBS", "Euro-Schatz", "EUREX", currency="EUR", contract_size=100000, contract_size_unit="EUR face value", tick_size=0.005, tick_value=5.0, **_BUND),
166 + _s("FGBX", "Euro-Buxl", "EUREX", currency="EUR", contract_size=100000, contract_size_unit="EUR face value", tick_size=0.02, tick_value=20.0, **_BUND),
167 + _s("FOAT", "Euro-OAT", "EUREX", currency="EUR", contract_size=100000, contract_size_unit="EUR face value", tick_size=0.01, tick_value=10.0, **_BUND),
168 + _s("FBTP", "Euro-BTP", "EUREX", currency="EUR", contract_size=100000, contract_size_unit="EUR face value", tick_size=0.01, tick_value=10.0, **_BUND),
169 + _s("FBTS", "Short-Term Euro-BTP", "EUREX", currency="EUR", contract_size=100000, contract_size_unit="EUR face value", tick_size=0.01, tick_value=10.0, **_BUND),
170 + _s("FBON", "Euro-Bono", "EUREX", currency="EUR", contract_size=100000, contract_size_unit="EUR face value", tick_size=0.01, tick_value=10.0, **_BUND),
171 + _s("FEU3", "Three-Month Euribor", "EUREX", currency="EUR", asset_class="rates", settlement_type="cash", month_cycle=QUARTERLY, calendar="eurex"),
172 + _s("G", "Long Gilt", "ICE Europe", currency="GBP", contract_size=100000, contract_size_unit="GBP face value", tick_size=0.01, tick_value=10.0,
173 + asset_class="rates", settlement_type="physical", month_cycle=QUARTERLY),
174 + _s("L", "3-Month Sterling", "ICE Europe", currency="GBP", asset_class="rates", settlement_type="cash", month_cycle=QUARTERLY),
175 + _s("ER", "3-Month Euribor", "ICE Europe", currency="EUR", asset_class="rates", settlement_type="cash", month_cycle=QUARTERLY),
176 + _s("SO3", "3-Month SONIA", "ICE Europe", currency="GBP", asset_class="rates", settlement_type="cash", month_cycle=QUARTERLY),
177 + # ---- grains / ags (CBOT, ICE) ---------------------------------------------------------------------
178 + _s("ZC", "Corn", "CBOT", contract_size=5000, contract_size_unit="bushels", tick_size=0.25, tick_value=12.5, month_cycle="HKNUZ", **_GR),
179 + _s("ZS", "Soybeans", "CBOT", contract_size=5000, contract_size_unit="bushels", tick_size=0.25, tick_value=12.5, month_cycle="FHKNQUX", **_GR),
180 + _s("ZW", "Wheat (SRW)", "CBOT", contract_size=5000, contract_size_unit="bushels", tick_size=0.25, tick_value=12.5, month_cycle="HKNUZ", **_GR),
181 + _s("KE", "KC Hard Red Winter Wheat", "CBOT", contract_size=5000, contract_size_unit="bushels", tick_size=0.25, tick_value=12.5, month_cycle="HKNUZ", **_GR),
182 + _s("ZM", "Soybean Meal", "CBOT", contract_size=100, contract_size_unit="short tons", tick_size=0.10, tick_value=10.0, month_cycle="FHKNQUVZ", **_GR),
183 + _s("ZL", "Soybean Oil", "CBOT", contract_size=60000, contract_size_unit="lbs", tick_size=0.01, tick_value=6.0, month_cycle="FHKNQUVZ", **_GR),
184 + _s("ZO", "Oats", "CBOT", contract_size=5000, contract_size_unit="bushels", tick_size=0.25, tick_value=12.5, month_cycle="HKNUZ", **_GR),
185 + _s("ZR", "Rough Rice", "CBOT", contract_size=2000, contract_size_unit="cwt", tick_size=0.005, tick_value=10.0, month_cycle="FHKNUX", **_GR),
186 + _s("XC", "Mini Corn", "CBOT", contract_size=1000, contract_size_unit="bushels", tick_size=0.125, tick_value=1.25, month_cycle="HKNUZ", **_GR),
187 + _s("RS", "Canola", "ICE US", currency="CAD", contract_size=20, contract_size_unit="metric tons", tick_size=0.10, tick_value=2.0, month_cycle="FHKNX", **_GR),
188 + _s("EBM", "Milling Wheat", "Euronext", currency="EUR", contract_size=50, contract_size_unit="metric tons", tick_size=0.25, tick_value=12.5,
189 + asset_class="ags", settlement_type="physical", month_cycle="HKUZ", calendar="eurex"),
190 + _s("LBS", "Random Length Lumber", "CME", contract_size=110000, contract_size_unit="board feet", tick_size=0.10, tick_value=11.0,
191 + asset_class="ags", settlement_type="physical", month_cycle="FHKNUX"),
192 + # ---- livestock / dairy (CME) ----------------------------------------------------------------------
193 + _s("LE", "Live Cattle", "CME", contract_size=40000, contract_size_unit="lbs", tick_size=0.025, tick_value=10.0,
194 + asset_class="livestock", settlement_type="physical", month_cycle="GJMQVZ", expiry_rule="last_business_day", rth=("09:30", "14:05")),
195 + _s("HE", "Lean Hogs", "CME", contract_size=40000, contract_size_unit="lbs", tick_size=0.025, tick_value=10.0,
196 + asset_class="livestock", settlement_type="cash", month_cycle="GJKMNQVZ", expiry_rule="he_rule", rth=("09:30", "14:05")),
197 + _s("GF", "Feeder Cattle", "CME", contract_size=50000, contract_size_unit="lbs", tick_size=0.025, tick_value=12.5,
198 + asset_class="livestock", settlement_type="cash", month_cycle="FHJKQUVX", expiry_rule="gf_rule", rth=("09:30", "14:05")),
199 + _s("PRK", "Pork Cutout", "CME", contract_size=40000, contract_size_unit="lbs", tick_size=0.025, tick_value=10.0,
200 + asset_class="livestock", settlement_type="cash", month_cycle="GJKMNQVZ", expiry_rule="he_rule", rth=("09:30", "14:05")),
201 + _s("DC", "Class III Milk", "CME", contract_size=200000, contract_size_unit="lbs", tick_size=0.01, tick_value=20.0,
202 + asset_class="ags", settlement_type="cash", month_cycle=ALL_MONTHS),
203 + _s("CB", "Cash-Settled Butter", "CME", contract_size=20000, contract_size_unit="lbs", asset_class="ags", settlement_type="cash", month_cycle=ALL_MONTHS),
204 + _s("CSC", "Cash-Settled Cheese", "CME", contract_size=20000, contract_size_unit="lbs", asset_class="ags", settlement_type="cash", month_cycle=ALL_MONTHS),
205 + # ---- softs (ICE) ----------------------------------------------------------------------------------
206 + _s("KC", "Coffee C", "ICE US", contract_size=37500, contract_size_unit="lbs", tick_size=0.05, tick_value=18.75,
207 + asset_class="softs", settlement_type="physical", month_cycle="HKNUZ", expiry_rule="kc_rule", first_notice_rule="kc", rth=("04:15", "13:30")),
208 + _s("SB", "Sugar No. 11", "ICE US", contract_size=112000, contract_size_unit="lbs", tick_size=0.01, tick_value=11.2,
209 + asset_class="softs", settlement_type="physical", month_cycle="HKNV", expiry_rule="prior_month_last_business_day", rth=("03:30", "13:00")),
210 + _s("CC", "Cocoa", "ICE US", contract_size=10, contract_size_unit="metric tons", tick_size=1.0, tick_value=10.0,
211 + asset_class="softs", settlement_type="physical", month_cycle="HKNUZ", expiry_rule="cc_rule", rth=("04:45", "13:30")),
212 + _s("CT", "Cotton No. 2", "ICE US", contract_size=50000, contract_size_unit="lbs", tick_size=0.01, tick_value=5.0,
213 + asset_class="softs", settlement_type="physical", month_cycle="HKNVZ", expiry_rule="ct_rule", rth=("21:00", "14:20")),
214 + _s("OJ", "FCOJ-A", "ICE US", contract_size=15000, contract_size_unit="lbs", tick_size=0.05, tick_value=7.5,
215 + asset_class="softs", settlement_type="physical", month_cycle="FHKNUX", expiry_rule="oj_rule", rth=("08:00", "14:00")),
216 + _s("C", "London Cocoa", "ICE Europe", currency="GBP", contract_size=10, contract_size_unit="metric tons", tick_size=1.0, tick_value=10.0,
217 + asset_class="softs", settlement_type="physical", month_cycle="HKNUZ", expiry_rule="cc_rule", rth=("04:45", "12:30")),
218 + _s("RM", "Robusta Coffee", "ICE Europe", contract_size=10, contract_size_unit="metric tons", tick_size=1.0, tick_value=10.0,
219 + asset_class="softs", settlement_type="physical", month_cycle="FHKNUX", rth=("04:00", "12:30")),
220 + # ---- FX (CME, ICE) --------------------------------------------------------------------------------
221 + _s("E6", "Euro FX", "CME", contract_size=125000, contract_size_unit="EUR", tick_size=0.00005, tick_value=6.25, **_FX),
222 + _s("J1", "Japanese Yen", "CME", contract_size=12500000, contract_size_unit="JPY", tick_size=0.0000005, tick_value=6.25, **_FX),
223 + _s("B6", "British Pound", "CME", contract_size=62500, contract_size_unit="GBP", tick_size=0.0001, tick_value=6.25, **_FX),
224 + _s("A6", "Australian Dollar", "CME", contract_size=100000, contract_size_unit="AUD", tick_size=0.00005, tick_value=5.0, **_FX),
225 + _s("AD", "Canadian Dollar", "CME", contract_size=100000, contract_size_unit="CAD", tick_size=0.00005, tick_value=5.0, **_FX),
226 + _s("E1", "Swiss Franc", "CME", contract_size=125000, contract_size_unit="CHF", tick_size=0.00005, tick_value=6.25, **_FX),
227 + _s("N6", "New Zealand Dollar", "CME", contract_size=100000, contract_size_unit="NZD", tick_size=0.00005, tick_value=5.0, **_FX),
228 + _s("MP", "Mexican Peso", "CME", contract_size=500000, contract_size_unit="MXN", tick_size=0.00001, tick_value=5.0, **{**_FX, "month_cycle": ALL_MONTHS}),
229 + _s("T6", "South African Rand", "CME", contract_size=500000, contract_size_unit="ZAR", tick_size=0.00001, tick_value=5.0, **{**_FX, "month_cycle": ALL_MONTHS}),
230 + _s("BR", "Brazilian Real", "CME", contract_size=100000, contract_size_unit="BRL", tick_size=0.00005, tick_value=5.0,
231 + asset_class="fx", settlement_type="cash", month_cycle=ALL_MONTHS, expiry_rule="prior_month_last_business_day", rth=("08:20", "15:00")),
232 + _s("E7", "E-mini Euro FX", "CME", contract_size=62500, contract_size_unit="EUR", tick_size=0.0001, tick_value=6.25, **_FX),
233 + _s("J7", "E-mini Japanese Yen", "CME", contract_size=6250000, contract_size_unit="JPY", tick_size=0.000001, tick_value=6.25, **_FX),
234 + _s("NOK", "Norwegian Krone", "CME", contract_size=2000000, contract_size_unit="NOK", tick_size=0.00001, tick_value=20.0, **_FX),
235 + _s("SEK", "Swedish Krona", "CME", contract_size=2000000, contract_size_unit="SEK", tick_size=0.00001, tick_value=20.0, **_FX),
236 + _s("RP", "Euro / British Pound", "CME", currency="GBP", contract_size=125000, contract_size_unit="EUR", tick_size=0.00005, tick_value=6.25, **_FX),
237 + _s("RY", "Euro / Japanese Yen", "CME", currency="JPY", contract_size=125000, contract_size_unit="EUR", tick_size=0.01, tick_value=1250.0, **_FX),
238 + _s("PJY", "British Pound / Japanese Yen", "CME", currency="JPY", contract_size=125000, contract_size_unit="GBP", tick_size=0.01, tick_value=1250.0, **_FX),
239 + _s("CNH", "USD / Offshore RMB", "CME", currency="CNH", contract_size=100000, contract_size_unit="USD", tick_size=0.0001, tick_value=10.0,
240 + asset_class="fx", settlement_type="cash", month_cycle=ALL_MONTHS, expiry_rule="fx_rule", rth=("08:20", "15:00")),
241 + _s("KRW", "Korean Won", "CME", contract_size=125000000, contract_size_unit="KRW", asset_class="fx", settlement_type="cash", month_cycle=ALL_MONTHS),
242 + _s("SIR", "Indian Rupee", "CME", contract_size=5000000, contract_size_unit="INR", asset_class="fx", settlement_type="cash", month_cycle=ALL_MONTHS),
243 + _s("DX", "US Dollar Index", "ICE US", contract_size=1000, contract_size_unit="USD x index", tick_size=0.005, tick_value=5.0,
244 + asset_class="fx", settlement_type="cash", month_cycle=QUARTERLY, expiry_rule="fx_rule", rth=("08:20", "15:00")),
245 + # ---- volatility (Cboe) ----------------------------------------------------------------------------
246 + _s("VX", "VIX Futures", "CFE", contract_size=1000, contract_size_unit="USD x index", tick_size=0.05, tick_value=50.0,
247 + asset_class="volatility", settlement_type="cash", month_cycle=ALL_MONTHS, expiry_rule="vx_rule"),
248 + _s("VXM", "Mini VIX Futures", "CFE", contract_size=100, contract_size_unit="USD x index", tick_size=0.05, tick_value=5.0,
249 + asset_class="volatility", settlement_type="cash", month_cycle=ALL_MONTHS, expiry_rule="vx_rule"),
250 + # ---- crypto (CME) ---------------------------------------------------------------------------------
251 + _s("BTC", "Bitcoin", "CME", contract_size=5, contract_size_unit="BTC", tick_size=5.0, tick_value=25.0,
252 + asset_class="crypto", settlement_type="cash", month_cycle=ALL_MONTHS, expiry_rule="last_friday", rth=("08:00", "17:00")),
253 + _s("MBT", "Micro Bitcoin", "CME", contract_size=0.1, contract_size_unit="BTC", tick_size=5.0, tick_value=0.5,
254 + asset_class="crypto", settlement_type="cash", month_cycle=ALL_MONTHS, expiry_rule="last_friday", rth=("08:00", "17:00")),
255 + _s("MET", "Micro Ether", "CME", contract_size=0.1, contract_size_unit="ETH", tick_size=0.5, tick_value=0.05,
256 + asset_class="crypto", settlement_type="cash", month_cycle=ALL_MONTHS, expiry_rule="last_friday", rth=("08:00", "17:00")),
257 +]
258 +
259 +SPEC_BY_ROOT: dict[str, RootSpec] = {s.root: s for s in SPECS}
260 +
261 +# Roots seen in the lake without a reference entry (2026-09): CPO, FBTM, FID, FNMY, FOAM, JB, JG, NK, RU,
262 +# ST, TWN, ZK — unknown products (not in meta/futures/futures.csv either). They get `derived` specs.
263 +UNKNOWN_LAKE_ROOTS = ("CPO", "FBTM", "FID", "FNMY", "FOAM", "JB", "JG", "NK", "RU", "ST", "TWN", "ZK")
264 +
265 +
266 +def derived_spec(root: str, name: str | None = None, exchange: str | None = None) -> RootSpec:
267 + """Placeholder spec for a root that exists in the lake but not in the reference table."""
268 + return RootSpec(root=root, name=name, exchange=exchange, asset_class=None, expiry_rule="data", source="derived",
269 + aliases=ALIASES_OF_ROOT.get(root, []))
270 +
271 +
272 +def spec_for(root: str) -> RootSpec:
273 + return SPEC_BY_ROOT.get(root) or derived_spec(root)
274 +
275 +
276 +def rth_window(root: str) -> tuple[str, str]:
277 + return spec_for(root).rth
added hfmarketdata/api/futures/symbols.py +118 −0
@@ -0,0 +1,118 @@
1 +"""Contract symbol parsing and root aliasing.
2 +
3 +Accepted inputs (case-insensitive, optional `_`/`-`/space between root and month):
4 +`ESZ25`, `ESZ2025`, `ES_Z25`, `esz25`, `6EZ25`, `M2KZ25`, `FDAXZ2025`.
5 +
6 +The lake (FirstRate Data) names CME FX products `E6`, `J1`, `B6`, `A6`, `AD`, `E1`, `N6`, `MP`, `BR`,
7 +`T6` and the 30-year T-bond `US`. Those lake names are the canonical roots of this module; the
8 +usual CME codes (`6E`, `6J`, `6B`, `6A`, `6C`, `6S`, `6N`, `6M`, `6L`, `6Z`, `ZB`) are accepted as
9 +aliases and normalised. `canonical_symbol()` always returns the short lake form (`E6Z25`).
10 +
11 +Two-digit years: 00–79 → 20xx, 80–99 → 19xx.
12 +
13 +Author: Simon-Pierre Boucher <contact@spboucher.ai>
14 +"""
15 +from __future__ import annotations
16 +
17 +import re
18 +from typing import NamedTuple
19 +
20 +from core.errors import ApiError
21 +
22 +MONTH_CODES = "FGHJKMNQUVXZ"
23 +MONTH_OF_CODE: dict[str, int] = {c: i + 1 for i, c in enumerate(MONTH_CODES)}
24 +CODE_OF_MONTH: dict[int, str] = {v: k for k, v in MONTH_OF_CODE.items()}
25 +
26 +# CME / common code -> FirstRate lake root (canonical here).
27 +ROOT_ALIASES: dict[str, str] = {
28 + "6E": "E6", # Euro FX
29 + "6J": "J1", # Japanese Yen (full size). FirstRate `J7` is the E-mini yen (CME E7-style mini) — not 6J.
30 + "6B": "B6", # British Pound
31 + "6A": "A6", # Australian Dollar
32 + "6C": "AD", # Canadian Dollar
33 + "6S": "E1", # Swiss Franc
34 + "6N": "N6", # New Zealand Dollar
35 + "6M": "MP", # Mexican Peso
36 + "6L": "BR", # Brazilian Real
37 + "6Z": "T6", # South African Rand
38 + "ZB": "US", # 30-Year T-Bond (CBOT code ZB; FirstRate keeps the old `US` root)
39 + "EMD": "EW", # E-mini S&P MidCap 400 (FirstRate `EW`)
40 + "BRN": "B", # ICE Brent (FirstRate `B`)
41 +}
42 +# Not aliased on purpose (no equivalent in the lake): 6R (ruble), QM (E-mini crude), 6N-like exotics
43 +# are already covered. FirstRate `J7`/`E7` are the E-mini yen / E-mini euro, distinct products.
44 +
45 +# Reverse map: canonical lake root -> list of accepted aliases (for /roots output).
46 +ALIASES_OF_ROOT: dict[str, list[str]] = {}
47 +for _alias, _root in ROOT_ALIASES.items():
48 + ALIASES_OF_ROOT.setdefault(_root, []).append(_alias)
49 +
50 +_SYMBOL_RE = re.compile(r"^([A-Z0-9]{1,4})[ _\-]?([FGHJKMNQUVXZ])(\d{2}|\d{4})$")
51 +
52 +
53 +class ContractSymbol(NamedTuple):
54 + root: str # canonical lake root (E6, ES, US…)
55 + month_code: str # F G H J K M N Q U V X Z
56 + year: int # four digits
57 +
58 + @property
59 + def month(self) -> int:
60 + return MONTH_OF_CODE[self.month_code]
61 +
62 + @property
63 + def short(self) -> str:
64 + """Lake form, e.g. `ESZ25`."""
65 + return f"{self.root}{self.month_code}{self.year % 100:02d}"
66 +
67 + @property
68 + def long(self) -> str:
69 + return f"{self.root}{self.month_code}{self.year}"
70 +
71 + @property
72 + def file_stem(self) -> str:
73 + """`ES_Z25` — prefix of the lake file names (`ES_Z25_1day.parquet`)."""
74 + return f"{self.root}_{self.month_code}{self.year % 100:02d}"
75 +
76 +
77 +def normalize_root(root: str) -> str:
78 + """Upper-case + alias resolution. Does not check existence in the lake."""
79 + r = (root or "").strip().upper()
80 + return ROOT_ALIASES.get(r, r)
81 +
82 +
83 +def expand_year(yy: int) -> int:
84 + if yy >= 100:
85 + return yy
86 + return 2000 + yy if yy <= 79 else 1900 + yy
87 +
88 +
89 +def parse_symbol(symbol: str) -> ContractSymbol:
90 + """Parse `ESZ25` / `ESZ2025` / `ES_Z25` / `6EZ25` → ContractSymbol. Raises 400 INVALID_CONTRACT_SYMBOL."""
91 + raw = (symbol or "").strip().upper()
92 + m = _SYMBOL_RE.match(raw)
93 + if not m:
94 + raise ApiError(400, "INVALID_CONTRACT_SYMBOL",
95 + f"'{symbol}' is not a valid contract symbol. Expected <ROOT><MONTH><YY|YYYY>, "
96 + "e.g. ESZ25, ESZ2025, ES_Z25, 6EZ25 (month codes F G H J K M N Q U V X Z).",
97 + details={"symbol": symbol, "month_codes": MONTH_CODES})
98 + root, mc, yy = m.groups()
99 + year = expand_year(int(yy))
100 + if not 1980 <= year <= 2099:
101 + raise ApiError(400, "INVALID_CONTRACT_SYMBOL", f"'{symbol}': year {year} is out of range (1980–2099).",
102 + details={"symbol": symbol})
103 + return ContractSymbol(normalize_root(root), mc, year)
104 +
105 +
106 +def canonical_symbol(symbol: str) -> str:
107 + return parse_symbol(symbol).short
108 +
109 +
110 +def symbol_from_file_stem(stem: str) -> ContractSymbol | None:
111 + """`ES_Z24_1day` or `ES_Z24` → ContractSymbol (None if the name is not a contract file)."""
112 + parts = stem.split("_")
113 + if len(parts) < 2:
114 + return None
115 + root, tail = parts[0].upper(), parts[1].upper()
116 + if len(tail) != 3 or tail[0] not in MONTH_OF_CODE or not tail[1:].isdigit():
117 + return None
118 + return ContractSymbol(root, tail[0], expand_year(int(tail[1:])))
119