mcx_history.py — Trading Calendar & Holiday Builder
MCX publishes its holiday list for the current year only — no year
selector, no archive. This module reconstructs the full 2003→today calendar
from a different, undocumented endpoint, and exposes MCXWorkingDays() for
every other module that needs to know which dates MCX actually traded.
from mcxeod.mcx_history import update_mcx_holidays, MCXWorkingDays
update_mcx_holidays() # full 2003 -> today rebuild
days = MCXWorkingDays(date(2020, 1, 1), date(2020, 12, 31))
cd backend
python -m mcxeod.mcx_history # rebuild trading calendar + holidays
Rebuild annually each January.
Two Data Sources
| Source | Endpoint | Covers | Gives |
|---|---|---|---|
| Historical turnover | GetHistoricalDataDetails | 2003 → today, per commodity | Proof a date was open — no OHLC, only TradedContract / Quantity / TotalValue |
| Holiday page | trading-surveillance/trading-holidays | Current year only | Holiday names and per-session (morning_open / evening_open) status |
The historical endpoint can prove MCX was open; it can never give prices —
prices only ever come from the bhavcopy (mcx_downloader.py).
build_trading_calendar(start, end, client) → set[date]
Always take the union across anchors — never trust a single commodity.
GOLDM didn't exist until 2003-11-20 and was idle on 2003-11-21 even though
MCX was open that day; a GOLDM-only calendar would invent a holiday.
| Anchor | Trading from | Note |
|---|---|---|
GOLD | 2003-11-18 | Day one — alone matched the full union (6,328 days) |
SILVER | 2003-11-18 | Day one |
GOLDM | 2003-11-20 | Idle 2003-11-21 while MCX was open — unsafe alone |
CRUDEOIL | later | Modern redundancy |
The API rejects windows ≥360 days, so _chunks() splits any range into
355-day pages.
update_mcx_holidays(start, end, calendar, apply=True) → dict
Rebuilds the calendar rows for exch = 2 (MCX) in the shared holidays /
specialdays tables.
holidays(exch=2) — weekdays MCX did not tradespecialdays(exch=2) — weekends MCX did trade (Saturdays 2005–2012ish, Muhurat/budget Sundays)
Holiday name resolution order
- Current-year
holiday_page()scrape, if the date falls in this year _nse_holiday_names()— MCX full closures proved to be a subset of NSE holidays (holidaystable,exch=1); 96 of 101 historical names resolved this way"MCX Holiday (derived)"— used for the remaining 5 (all 2004–2006), each individually verified against that day's bhavcopy as a genuine closure. Never guess a name — leave the derived label and log it.
Session flags
morning_open / evening_open come from the current-year holiday page only.
They are NULL for history — an EOD turnover feed cannot say which session
traded; NULL honestly means "traded, sessions unknown". Only
morning_open = false AND evening_open = false counts as a fully blocked
working day.
Deletes and inserts are always scoped WHERE exch = 2 — an MCX rebuild must
never touch the NSE rows (exch = 1) that supply the fallback names.
nseeod/utils.py's updateholidays() deletes are exch-scoped tooIf that scoping is ever removed from the NSE side, the next NSE refresh destroys the MCX calendar built here.
MCXWorkingDays(fromdate, todate) → list[date]
The calendar-aware replacement for a weekday() < 5 filter, used by
mcx_backfill.py and anywhere else that needs a
historically-correct MCX trading range.
day in weekend_sessions OR (day.weekday() < 5 AND day not in holidays)
MCX ran 490 Saturday sessions (2004-07-03 → 2025-02-01) and 5 Sunday
sessions (Muhurat/budget days: 2013-11-03, 2016-10-30, 2019-10-27,
2023-11-12, 2026-02-01). A plain weekday() < 5 walk silently discards all
495 of them. McxProcessor.run_range_download() still does this — it's
scoped to the "sync recent dates" UI path only; do not reuse it for
historical work.
MCXHolidays() → list[str]
MCX non-trading dates as 'YYYY-MM-DD' strings, mirroring nseeod.utils.NseHolidays().
Calendar Facts (built 2026-08-17, local tedb)
| Trading days 2003-11-18 → 2026-08-14 | 6,328 |
Holidays (holidays, exch=2) | 101 (~4.4/yr — full closures only) |
| …of which unnamed (derived) | 5 (all 2004–2006, each bhavcopy-verified) |
Weekend sessions (specialdays, exch=2) | 495 (490 Sat + 5 Sun) |
Related
- mcx_downloader.py —
McxProcessor.run_range_download()uses a plain weekday filter, notMCXWorkingDays() - mcx_backfill.py — the primary consumer of
MCXWorkingDays()