Skip to content

trading_agent.scheduling.market_calendar

Which days the Indian exchanges trade, read from UBI's own exchange calendar files.

UBI keeps one YAML file a year, generated from the exchanges' own publications (NSE, BSE, MCX and NCDEX), listing each holiday by exchange and calendar and each special session such as Muhurat trading. Reading the same files keeps trading-agent and UBI in agreement about every holiday.

A calendar is a set of segments that close together: equity (cash, indices and equity derivatives), currency (currency derivatives, which also close on bank holidays) and commodity. A commodity holiday can close only the morning or only the evening session.

Typical usage example:

calendar = MarketCalendar(Path('.../calendars')) calendar.is_trading_day(datetime.date(2026, 10, 20)) # False: Dussehra

EQUITY module-attribute

EQUITY = 'equity'

CURRENCY module-attribute

CURRENCY = 'currency'

COMMODITY module-attribute

COMMODITY = 'commodity'

Closure dataclass

Closure(name, closed)

One holiday.

Attributes:

Name Type Description
name str

The holiday's name, as the exchange publishes it.

closed str

"all", or "morning" or "evening" for a commodity calendar.

SpecialSession dataclass

SpecialSession(name, opens, closes)

A session on a day that would otherwise be closed, such as Muhurat trading.

Attributes:

Name Type Description
name str

The session's name.

opens time

When it opens, India time.

closes time

When it closes, India time.

MarketCalendar

MarketCalendar(calendar_directory)

The exchanges' holidays and special sessions.

Reads every year's file in the folder.

A missing folder or unreadable file is logged; the calendar then knows weekends only, and the doctor reports it.

Parameters:

Name Type Description Default
calendar_directory Path | None

UBI's calendars folder, or None to know weekends only.

required
Source code in src/trading_agent/scheduling/market_calendar.py
def __init__(self, calendar_directory: Path | None):
    """Reads every year's file in the folder.

    A missing folder or unreadable file is logged; the calendar then knows weekends only, and the doctor reports it.

    Args:
        calendar_directory (Path | None): UBI's calendars folder, or None to know weekends only.
    """
    self._closures: dict[tuple[str, str, datetime.date], Closure] = {}
    self._special_sessions: dict[
        tuple[str, str, datetime.date], SpecialSession
    ] = {}
    self.years: list[int] = []
    if calendar_directory is None or not calendar_directory.is_dir():
        if calendar_directory is not None:
            _LOGGER.warning(
                'No exchange calendar folder at %s', calendar_directory
            )
        return
    for path in sorted(calendar_directory.glob('*.yaml')):
        try:
            self._read_year(path)
        except OSError, yaml.YAMLError, KeyError, TypeError, ValueError:
            _LOGGER.exception(
                'Could not read the exchange calendar %s', path
            )

calendar_for

calendar_for(exchange, segment)

The calendar an instrument's segment follows.

Parameters:

Name Type Description Default
exchange str

The exchange, such as nse or mcx.

required
segment str

UBI's segment, such as nse_equities or mcx_commodities.

required

Returns:

Type Description
str

"equity", "currency" or "commodity".

Source code in src/trading_agent/scheduling/market_calendar.py
def calendar_for(self, exchange: str, segment: str) -> str:
    """The calendar an instrument's segment follows.

    Args:
        exchange (str): The exchange, such as nse or mcx.
        segment (str): UBI's segment, such as nse_equities or mcx_commodities.

    Returns:
        str: "equity", "currency" or "commodity".
    """
    text = f'{exchange}_{segment}'.lower()
    if 'commodit' in text or exchange.lower() in ('mcx', 'ncdex'):
        return COMMODITY
    if 'currency' in text or exchange.lower() == 'cds':
        return CURRENCY
    return EQUITY

closure

closure(day, exchange='nse', calendar=EQUITY)

The holiday on a day, if there is one.

Parameters:

Name Type Description Default
day date

The date.

required
exchange str

The exchange.

'nse'
calendar str

"equity", "currency" or "commodity".

EQUITY

Returns:

Type Description
Closure | None

The holiday, or None.

Source code in src/trading_agent/scheduling/market_calendar.py
def closure(
    self,
    day: datetime.date,
    exchange: str = 'nse',
    calendar: str = EQUITY,
) -> Closure | None:
    """The holiday on a day, if there is one.

    Args:
        day (datetime.date): The date.
        exchange (str): The exchange.
        calendar (str): "equity", "currency" or "commodity".

    Returns:
        Closure | None: The holiday, or None.
    """
    return self._closures.get((exchange.lower(), calendar, day))

special_session

special_session(day, exchange='nse', calendar=EQUITY)

The special session on a day, if there is one.

Parameters:

Name Type Description Default
day date

The date.

required
exchange str

The exchange.

'nse'
calendar str

"equity", "currency" or "commodity".

EQUITY

Returns:

Type Description
SpecialSession | None

The session, or None.

Source code in src/trading_agent/scheduling/market_calendar.py
def special_session(
    self,
    day: datetime.date,
    exchange: str = 'nse',
    calendar: str = EQUITY,
) -> SpecialSession | None:
    """The special session on a day, if there is one.

    Args:
        day (datetime.date): The date.
        exchange (str): The exchange.
        calendar (str): "equity", "currency" or "commodity".

    Returns:
        SpecialSession | None: The session, or None.
    """
    return self._special_sessions.get((exchange.lower(), calendar, day))

is_trading_day

is_trading_day(day, exchange='nse', calendar=EQUITY)

Says whether any part of a day trades.

Parameters:

Name Type Description Default
day date

The date.

required
exchange str

The exchange.

'nse'
calendar str

"equity", "currency" or "commodity".

EQUITY

Returns:

Type Description
bool

True on a weekday that is not a full holiday, or on a day with a special session.

Source code in src/trading_agent/scheduling/market_calendar.py
def is_trading_day(
    self,
    day: datetime.date,
    exchange: str = 'nse',
    calendar: str = EQUITY,
) -> bool:
    """Says whether any part of a day trades.

    Args:
        day (datetime.date): The date.
        exchange (str): The exchange.
        calendar (str): "equity", "currency" or "commodity".

    Returns:
        bool: True on a weekday that is not a full holiday, or on a day with a special session.
    """
    if self.special_session(day, exchange, calendar) is not None:
        return True
    if day.weekday() >= _SATURDAY:
        return False
    closure = self.closure(day, exchange, calendar)
    return closure is None or closure.closed != 'all'

next_trading_day

next_trading_day(after, exchange='nse', calendar=EQUITY)

The first trading day after a date.

Parameters:

Name Type Description Default
after date

The date to count from, not itself included.

required
exchange str

The exchange.

'nse'
calendar str

"equity", "currency" or "commodity".

EQUITY

Returns:

Type Description
date

The next trading day.

Raises:

Type Description
ValueError

None of the next thirty days trades, which means the calendar is wrong.

Source code in src/trading_agent/scheduling/market_calendar.py
def next_trading_day(
    self,
    after: datetime.date,
    exchange: str = 'nse',
    calendar: str = EQUITY,
) -> datetime.date:
    """The first trading day after a date.

    Args:
        after (datetime.date): The date to count from, not itself included.
        exchange (str): The exchange.
        calendar (str): "equity", "currency" or "commodity".

    Returns:
        datetime.date: The next trading day.

    Raises:
        ValueError: None of the next thirty days trades, which means the calendar is wrong.
    """
    day = after
    for _ in range(_MOST_DAYS_AHEAD):
        day = day + datetime.timedelta(days=1)
        if self.is_trading_day(day, exchange, calendar):
            return day
    raise ValueError(
        f'No trading day in the {_MOST_DAYS_AHEAD} days after {after}'
    )