import sys
sys.path.append("../")
from datetime import time
import pandas as pd
import pandas_market_calendars as mcal

Calendars

Basic Usage

Setup new exchange calendar

nyse = mcal.get_calendar('NYSE')

Get the time zone

nyse.tz.zone
'America/New_York'

Get the AbstractHolidayCalendar object

holidays = nyse.holidays()
holidays.holidays[-5:]
(numpy.datetime64('2200-05-26'),
 numpy.datetime64('2200-07-04'),
 numpy.datetime64('2200-09-01'),
 numpy.datetime64('2200-11-27'),
 numpy.datetime64('2200-12-25'))

View the available information on regular market times

print(nyse.regular_market_times) # more on this under the 'Customizations' heading
ProtectedDict(
{'pre': ((None, datetime.time(4, 0)),),
 'market_open': ((None, datetime.time(10, 0)),
                 ('1985-01-01', datetime.time(9, 30))),
 'market_close': ((None, datetime.time(15, 0)),
                  ('1952-09-29', datetime.time(15, 30)),
                  ('1974-01-01', datetime.time(16, 0))),
 'post': ((None, datetime.time(20, 0)),)}
)

Exchange open valid business days

Get the valid open exchange business dates between a start and end date. Note that Dec 26 (Christmas), Jan 2 (New Years) and all weekends are missing

nyse.valid_days(start_date='2016-12-20', end_date='2017-01-10')
DatetimeIndex(['2016-12-20 00:00:00+00:00', '2016-12-21 00:00:00+00:00',
               '2016-12-22 00:00:00+00:00', '2016-12-23 00:00:00+00:00',
               '2016-12-27 00:00:00+00:00', '2016-12-28 00:00:00+00:00',
               '2016-12-29 00:00:00+00:00', '2016-12-30 00:00:00+00:00',
               '2017-01-03 00:00:00+00:00', '2017-01-04 00:00:00+00:00',
               '2017-01-05 00:00:00+00:00', '2017-01-06 00:00:00+00:00',
               '2017-01-09 00:00:00+00:00', '2017-01-10 00:00:00+00:00'],
              dtype='datetime64[ns, UTC]', freq=None)

Schedule

schedule = nyse.schedule(start_date='2016-12-30', end_date='2017-01-10')
schedule
market_open market_close
2016-12-30 2016-12-30 14:30:00+00:00 2016-12-30 21:00:00+00:00
2017-01-03 2017-01-03 14:30:00+00:00 2017-01-03 21:00:00+00:00
2017-01-04 2017-01-04 14:30:00+00:00 2017-01-04 21:00:00+00:00
2017-01-05 2017-01-05 14:30:00+00:00 2017-01-05 21:00:00+00:00
2017-01-06 2017-01-06 14:30:00+00:00 2017-01-06 21:00:00+00:00
2017-01-09 2017-01-09 14:30:00+00:00 2017-01-09 21:00:00+00:00
2017-01-10 2017-01-10 14:30:00+00:00 2017-01-10 21:00:00+00:00
# with early closes
early = nyse.schedule(start_date='2012-07-01', end_date='2012-07-10')
early
market_open market_close
2012-07-02 2012-07-02 13:30:00+00:00 2012-07-02 20:00:00+00:00
2012-07-03 2012-07-03 13:30:00+00:00 2012-07-03 17:00:00+00:00
2012-07-05 2012-07-05 13:30:00+00:00 2012-07-05 20:00:00+00:00
2012-07-06 2012-07-06 13:30:00+00:00 2012-07-06 20:00:00+00:00
2012-07-09 2012-07-09 13:30:00+00:00 2012-07-09 20:00:00+00:00
2012-07-10 2012-07-10 13:30:00+00:00 2012-07-10 20:00:00+00:00
# including pre and post-market
extended = nyse.schedule(start_date='2012-07-01', end_date='2012-07-10', start="pre", end="post")
extended
pre market_open market_close post
2012-07-02 2012-07-02 08:00:00+00:00 2012-07-02 13:30:00+00:00 2012-07-02 20:00:00+00:00 2012-07-03 00:00:00+00:00
2012-07-03 2012-07-03 08:00:00+00:00 2012-07-03 13:30:00+00:00 2012-07-03 17:00:00+00:00 2012-07-03 17:00:00+00:00
2012-07-05 2012-07-05 08:00:00+00:00 2012-07-05 13:30:00+00:00 2012-07-05 20:00:00+00:00 2012-07-06 00:00:00+00:00
2012-07-06 2012-07-06 08:00:00+00:00 2012-07-06 13:30:00+00:00 2012-07-06 20:00:00+00:00 2012-07-07 00:00:00+00:00
2012-07-09 2012-07-09 08:00:00+00:00 2012-07-09 13:30:00+00:00 2012-07-09 20:00:00+00:00 2012-07-10 00:00:00+00:00
2012-07-10 2012-07-10 08:00:00+00:00 2012-07-10 13:30:00+00:00 2012-07-10 20:00:00+00:00 2012-07-11 00:00:00+00:00
# specific market times
# CAVEAT: Looking at 2012-07-03, you can see that times will NOT be adjusted to special_opens/sepcial_closes
# if market_open/market_close are not requested
specific = nyse.schedule(start_date='2012-07-01', end_date='2012-07-10', market_times= ["post", "market_open"]) # this order will be kept
specific
post market_open
2012-07-02 2012-07-03 00:00:00+00:00 2012-07-02 13:30:00+00:00
2012-07-03 2012-07-04 00:00:00+00:00 2012-07-03 13:30:00+00:00
2012-07-05 2012-07-06 00:00:00+00:00 2012-07-05 13:30:00+00:00
2012-07-06 2012-07-07 00:00:00+00:00 2012-07-06 13:30:00+00:00
2012-07-09 2012-07-10 00:00:00+00:00 2012-07-09 13:30:00+00:00
2012-07-10 2012-07-11 00:00:00+00:00 2012-07-10 13:30:00+00:00

Get early closes

nyse.early_closes(schedule=early)
market_open market_close
2012-07-03 2012-07-03 13:30:00+00:00 2012-07-03 17:00:00+00:00
nyse.early_closes(schedule=extended)
pre market_open market_close post
2012-07-03 2012-07-03 08:00:00+00:00 2012-07-03 13:30:00+00:00 2012-07-03 17:00:00+00:00 2012-07-03 17:00:00+00:00

Open at time

Test to see if a given timestamp is during market open hours

nyse.open_at_time(early, pd.Timestamp('2012-07-03 12:00', tz='America/New_York'))
True
nyse.open_at_time(early, pd.Timestamp('2012-07-03 16:00', tz='America/New_York'))
False

Other market times will also be considered

nyse.open_at_time(extended, pd.Timestamp('2012-07-05 18:00', tz='America/New_York'))
True

but can be ignored by setting only_rth = True

nyse.open_at_time(extended, pd.Timestamp('2012-07-05 18:00', tz='America/New_York'), only_rth = True)
False

Customizations

The simplest way to customize the market times of a calendar is by passing datetime.time objects to the constructor, which will modify the open and/or close of regular trading hours.

cal = mcal.get_calendar('NYSE', open_time=time(10, 0), close_time=time(14, 30))
print('open, close: %s, %s' % (cal.open_time, cal.close_time))
open, close: 10:00:00, 14:30:00

More advanced customizations can be done after initialization or by inheriting from the closest MarketCalendar class, which requires an explanation of market times…

Market times

Market times are moments in a trading day that are contained in the regular_market_times attribute, for example:

print("The original NYSE calendar: \n", nyse.regular_market_times)
The original NYSE calendar:
 ProtectedDict(
{'pre': ((None, datetime.time(4, 0)),),
 'market_open': ((None, datetime.time(10, 0)),
                 ('1985-01-01', datetime.time(9, 30))),
 'market_close': ((None, datetime.time(15, 0)),
                  ('1952-09-29', datetime.time(15, 30)),
                  ('1974-01-01', datetime.time(16, 0))),
 'post': ((None, datetime.time(20, 0)),)}
)

NYSE’s regular trading hours are referenced by “market_open” and “market_close”, but NYSE also has extended hours, which are referenced by “pre” and “post”.

The attribute ``regular_market_times`` has these requirements:

  • It needs to be a dictionary
  • Each market_time needs one entry
    • Regular open must be “market_open”, regular close must be “market_close”.
    • If there is a break, there must be a “break_start” and a “break_end”.
    • only ONE break is currently supported.
  • One list/tuple for each market_time, containing at least one list/tuple:
    • Each nested iterable needs at least two items: (first_date_used, time[, offset]).
    • The first iterable’s date should be None, marking the start. In every iterable thereafter this is the date when time was first used.
    • Optionally (assumed to be zero, when not present), a positive or negative integer, representing an offset in number of days.
    • Dates need to be in ascending order, None coming first.

E.g.:

print(nyse.get_time("market_close", all_times= True)) # all_times = False only returns current
((None, datetime.time(15, 0)), ('1952-09-29', datetime.time(15, 30)), ('1974-01-01', datetime.time(16, 0)))

The first known close was 3pm, which changed on 1952-09-29 to 3:30pm, which changed on 1974-01-01 to 4pm. The dates are the first dates that the new time was used.

Customizing after initialization

There are three methods that allow customizing the regular_market_times of a MarketCalendar instance: * .change_time(market_time, times) * .add_time(market_time, times) * .remove_time(market_time)

cal = mcal.get_calendar("NYSE")
cal.change_time("market_open", time(10,30))
print('open, close: %s, %s' % (cal.open_time, cal.close_time))
print("\nThe 'market_open' information is entirely replaced:\n", cal.regular_market_times)
open, close: 10:30:00, 16:00:00

The 'market_open' information is entirely replaced:
 ProtectedDict(
{'pre': ((None, datetime.time(4, 0)),),
 'market_open': ((None, datetime.time(10, 30)),),
 'market_close': ((None, datetime.time(15, 0)),
                  ('1952-09-29', datetime.time(15, 30)),
                  ('1974-01-01', datetime.time(16, 0))),
 'post': ((None, datetime.time(20, 0)),)}
)
cal.remove_time("post")
cal.add_time("new_post", time(19))
print(cal.regular_market_times)
ProtectedDict(
{'pre': ((None, datetime.time(4, 0)),),
 'market_open': ((None, datetime.time(10, 30)),),
 'market_close': ((None, datetime.time(15, 0)),
                  ('1952-09-29', datetime.time(15, 30)),
                  ('1974-01-01', datetime.time(16, 0))),
 'new_post': ((None, datetime.time(19, 0)),)}
)
cal.remove_time("pre")
cal.remove_time("new_post")

The methods .add_time and .change_time also accept the time information in these formats:

cal.add_time("just_time", time(10))
cal.add_time("with_offset", (time(10), -1))
cal.add_time("changes_and_offset", ((None, time(17)), ("2009-12-28", time(11), -2)))
print(cal.regular_market_times)
ProtectedDict(
{'market_open': ((None, datetime.time(10, 30)),),
 'market_close': ((None, datetime.time(15, 0)),
                  ('1952-09-29', datetime.time(15, 30)),
                  ('1974-01-01', datetime.time(16, 0))),
 'just_time': ((None, datetime.time(10, 0)),),
 'with_offset': ((None, datetime.time(10, 0), -1),),
 'changes_and_offset': ((None, datetime.time(17, 0)),
                        ('2009-12-28', datetime.time(11, 0), -2))}
)

CAVEATS:

FIRST

Internally, a an order of market_times is detected based on their current time.
Because of the offsets in “with_offset” and “changes_and_offset”, the columns in a schedule are in the following order:
cal.schedule("2009-12-23", "2009-12-29", market_times= "all")
changes_and_offset with_offset just_time market_open market_close
2009-12-23 2009-12-23 22:00:00+00:00 2009-12-22 15:00:00+00:00 2009-12-23 15:00:00+00:00 2009-12-23 15:30:00+00:00 2009-12-23 21:00:00+00:00
2009-12-24 2009-12-24 18:00:00+00:00 2009-12-23 15:00:00+00:00 2009-12-24 15:00:00+00:00 2009-12-24 15:30:00+00:00 2009-12-24 18:00:00+00:00
2009-12-28 2009-12-26 16:00:00+00:00 2009-12-27 15:00:00+00:00 2009-12-28 15:00:00+00:00 2009-12-28 15:30:00+00:00 2009-12-28 21:00:00+00:00
2009-12-29 2009-12-27 16:00:00+00:00 2009-12-28 15:00:00+00:00 2009-12-29 15:00:00+00:00 2009-12-29 15:30:00+00:00 2009-12-29 21:00:00+00:00

On 2009-12-23 changes_and_offset doesn’t seem to be in the right order, but as of 2009-12-28 it is.

Passing a list to market_times, allows you to keep a custom order:

cal.schedule("2009-12-23", "2009-12-29", market_times= ["with_offset", "market_open", "market_close", "changes_and_offset"])
with_offset market_open market_close changes_and_offset
2009-12-23 2009-12-22 15:00:00+00:00 2009-12-23 15:30:00+00:00 2009-12-23 21:00:00+00:00 2009-12-23 22:00:00+00:00
2009-12-24 2009-12-23 15:00:00+00:00 2009-12-24 15:30:00+00:00 2009-12-24 18:00:00+00:00 2009-12-24 18:00:00+00:00
2009-12-28 2009-12-27 15:00:00+00:00 2009-12-28 15:30:00+00:00 2009-12-28 21:00:00+00:00 2009-12-26 16:00:00+00:00
2009-12-29 2009-12-28 15:00:00+00:00 2009-12-29 15:30:00+00:00 2009-12-29 21:00:00+00:00 2009-12-27 16:00:00+00:00

SECOND

Special closes of market_closes will override all later times, special opens of market_opens will override all earlier times.
In the prior schedule, 2009-12-24 is a special market_close, which was enforced in the changes_and_offset column.

Providing False or None to the force_special_times keyword argument, changes this behaviour:

# False - will only adjust the columns itself (changes_and_offset left alone, market_close adjusted)
cal.schedule("2009-12-23", "2009-12-28", market_times= ["changes_and_offset", "market_close"], force_special_times= False)
changes_and_offset market_close
2009-12-23 2009-12-23 22:00:00+00:00 2009-12-23 21:00:00+00:00
2009-12-24 2009-12-24 22:00:00+00:00 2009-12-24 18:00:00+00:00
2009-12-28 2009-12-26 16:00:00+00:00 2009-12-28 21:00:00+00:00
# None - will not adjust any column (both are left alone)
cal.schedule("2009-12-23", "2009-12-28", market_times= ["changes_and_offset", "market_close"], force_special_times= None)
changes_and_offset market_close
2009-12-23 2009-12-23 22:00:00+00:00 2009-12-23 21:00:00+00:00
2009-12-24 2009-12-24 22:00:00+00:00 2009-12-24 21:00:00+00:00
2009-12-28 2009-12-26 16:00:00+00:00 2009-12-28 21:00:00+00:00

Inheriting from a MarketCalendar

You get even more control over a calendar (or help this package by contributing a calendar) by inheriting from a MarketCalendar class, which is as simple as this:

# CFEExchangeCalendar only has the regular trading hours for the futures exchange (8:30 - 15:15).
# But you want to use the equity options exchange (8:30 - 15:00), including the order acceptance time at 7:30.
# Maybe you even know some special cases when the order acceptance time was different....

from pandas_market_calendars.exchange_calendar_cboe import CFEExchangeCalendar

class DemoOptionsExchangeCalendar(CFEExchangeCalendar):  # Inherit what doesn't need to change
    name = "Demo_CBOE_Equity_Options"
    aliases = [name]
    regular_market_times = {**CFEExchangeCalendar.regular_market_times, # unpack the parent's regular_market_times
                            "order_acceptance": ((None, time(7,30)),),  # add your market time of interest
                            "market_close": ((None, time(15)),)} # overwrite the market time you want to change

    @property  ## See the 'Special times' header below
    def special_order_acceptance_adhoc(self):    # use the special_{market_time}_adhoc functionality to include special cases
        return [(time(8,30), ["2000-12-27", "2001-12-27"])]
options = mcal.get_calendar("Demo_CBOE_Equity_Options")

print(options.regular_market_times)
ProtectedDict(
{'market_open': ((None, datetime.time(8, 30)),),
 'market_close': ((None, datetime.time(15, 0)),),
 'order_acceptance': ((None, datetime.time(7, 30)),)}
)
schedule = options.schedule("2000-12-22", "2000-12-28", start= "order_acceptance")
schedule
order_acceptance market_open market_close
2000-12-22 2000-12-22 13:30:00+00:00 2000-12-22 14:30:00+00:00 2000-12-22 21:00:00+00:00
2000-12-26 2000-12-26 13:30:00+00:00 2000-12-26 14:30:00+00:00 2000-12-26 21:00:00+00:00
2000-12-27 2000-12-27 14:30:00+00:00 2000-12-27 14:30:00+00:00 2000-12-27 21:00:00+00:00
2000-12-28 2000-12-28 13:30:00+00:00 2000-12-28 14:30:00+00:00 2000-12-28 21:00:00+00:00

Dec 25th is filtered out already because it is inherited from the CFEExchangeCalendar, and the special case on 2000-12-27 is also integrated

Special times

Any market_time in regular_market_times can have special times, which are looked for in two properties: * special_{market_time}_adhoc * same format as special_opens_adhoc, which is the same as special_market_open_adhoc * special_{market_time} * same format as special_opens, which is the same as special_market_open

Advanced Usage

Checking for special times

The following functions respect varying times in regular_market_times

These will only check market_close/market_open columns for early/late times

options.early_closes(schedule), options.late_opens(schedule)
(Empty DataFrame
 Columns: [order_acceptance, market_open, market_close]
 Index: [],
 Empty DataFrame
 Columns: [order_acceptance, market_open, market_close]
 Index: [])

The is_different method uses the name of the series passed to it, to determine which rows are not equal to the regular market times, and return a boolean Series

schedule[options.is_different(schedule["order_acceptance"])]
order_acceptance market_open market_close
2000-12-27 2000-12-27 14:30:00+00:00 2000-12-27 14:30:00+00:00 2000-12-27 21:00:00+00:00

You can also pass pd.Series.lt/ -.gt / -.ge / etc. for more control over the comparison

schedule[options.is_different(schedule["order_acceptance"], pd.Series.lt)]
order_acceptance market_open market_close
schedule[options.is_different(schedule["order_acceptance"], pd.Series.ge)]
order_acceptance market_open market_close
2000-12-22 2000-12-22 13:30:00+00:00 2000-12-22 14:30:00+00:00 2000-12-22 21:00:00+00:00
2000-12-26 2000-12-26 13:30:00+00:00 2000-12-26 14:30:00+00:00 2000-12-26 21:00:00+00:00
2000-12-27 2000-12-27 14:30:00+00:00 2000-12-27 14:30:00+00:00 2000-12-27 21:00:00+00:00
2000-12-28 2000-12-28 13:30:00+00:00 2000-12-28 14:30:00+00:00 2000-12-28 21:00:00+00:00

Checking custom times

options.has_custom # order_acceptance is not considered custom because it is hardcoded into the class
False
options.add_time("post", time(17))
options.has_custom, options.is_custom("market_open"), options.is_custom("post")
(True, False, True)

Get the regular time on a certain date

nyse.open_time, nyse.close_time  # these always refer to the current time of market_open/market_close
(datetime.time(9, 30, tzinfo=<DstTzInfo 'America/New_York' LMT-1 day, 19:04:00 STD>),
 datetime.time(16, 0, tzinfo=<DstTzInfo 'America/New_York' LMT-1 day, 19:04:00 STD>))
nyse.get_time("post"), nyse.get_time("pre")  # these also refer to the current time
(datetime.time(20, 0, tzinfo=<DstTzInfo 'America/New_York' LMT-1 day, 19:04:00 STD>),
 datetime.time(4, 0, tzinfo=<DstTzInfo 'America/New_York' LMT-1 day, 19:04:00 STD>))
# open_time_on looks for market_open, close_time_on looks for market_close and get_time_on looks for the provided market time
nyse.open_time_on("1950-01-01"), nyse.get_time_on("market_close", "1960-01-01")
(datetime.time(10, 0, tzinfo=<DstTzInfo 'America/New_York' LMT-1 day, 19:04:00 STD>),
 datetime.time(15, 30, tzinfo=<DstTzInfo 'America/New_York' LMT-1 day, 19:04:00 STD>))

Special Methods

nyse["market_open"] # gets the current time
datetime.time(9, 30, tzinfo=<DstTzInfo 'America/New_York' LMT-1 day, 19:04:00 STD>)
nyse["market_open", "all"] # gets all times
((None, datetime.time(10, 0)), ('1985-01-01', datetime.time(9, 30)))
nyse["market_open", "1950-01-01"] # gets the time on a certain date
datetime.time(10, 0, tzinfo=<DstTzInfo 'America/New_York' LMT-1 day, 19:04:00 STD>)

This tries to add a time, which will fail if it already exists. In that case .change_time is the explicit alternative.

nyse["new_post"] = time(20)
nyse["new_post"]
datetime.time(20, 0, tzinfo=<DstTzInfo 'America/New_York' LMT-1 day, 19:04:00 STD>)
try: nyse["post"] = time(19)
except AssertionError as e: print(e)
post is already in regular_market_times:
['pre', 'market_open', 'market_close', 'post', 'new_post']

Array of special times

options.special_dates("order_acceptance", "2000-12-22", "2001-12-28")
DatetimeIndex(['2000-12-27 14:30:00+00:00', '2001-12-27 14:30:00+00:00'], dtype='datetime64[ns, UTC]', freq=None)

Handling discontinued times

xkrx = mcal.get_calendar("XKRX")
c:RMBAriesgitpandas_market_calendarspandas_market_calendarsmarket_calendar.py:128: UserWarning: ['break_start', 'break_end'] are discontinued, the dictionary .discontinued_market_times has the dates on which these were discontinued. The times as of those dates are incorrect, use .remove_time(market_time) to ignore a market_time.
  warnings.warn(f"{list(self.discontinued_market_times.keys())} are discontinued, the dictionary"
xkrx.schedule("2020-01-01", "2020-01-05")
market_open break_start break_end market_close
2020-01-02 2020-01-02 00:00:00+00:00 2020-01-02 03:00:00+00:00 2020-01-02 04:00:00+00:00 2020-01-02 06:30:00+00:00
2020-01-03 2020-01-03 00:00:00+00:00 2020-01-03 03:00:00+00:00 2020-01-03 04:00:00+00:00 2020-01-03 06:30:00+00:00
xkrx.discontinued_market_times # these are the dates as of which the market time didn't exist anymore
{'break_start': Timestamp('2000-05-22 00:00:00'),
 'break_end': Timestamp('2000-05-22 00:00:00')}
print(xkrx.has_discontinued)
xkrx.remove_time("break_start")
xkrx.remove_time("break_end")
print(xkrx.has_discontinued)
True
False
xkrx.schedule("2020-01-01", "2020-01-05")
market_open market_close
2020-01-02 2020-01-02 00:00:00+00:00 2020-01-02 06:30:00+00:00
2020-01-03 2020-01-03 00:00:00+00:00 2020-01-03 06:30:00+00:00

Helpers

schedules with columns other than market_open, break_start, break_end or market_close are not yet supported by the following functions

Date Range

This function will take a schedule DataFrame and return a DatetimeIndex with all timestamps at the frequency given for all of the exchange open dates and times.

mcal.date_range(early, frequency='1D')
DatetimeIndex(['2012-07-02 20:00:00+00:00', '2012-07-03 17:00:00+00:00',
               '2012-07-05 20:00:00+00:00', '2012-07-06 20:00:00+00:00',
               '2012-07-09 20:00:00+00:00', '2012-07-10 20:00:00+00:00'],
              dtype='datetime64[ns, UTC]', freq=None)
mcal.date_range(early, frequency='1H')
DatetimeIndex(['2012-07-02 14:30:00+00:00', '2012-07-02 15:30:00+00:00',
               '2012-07-02 16:30:00+00:00', '2012-07-02 17:30:00+00:00',
               '2012-07-02 18:30:00+00:00', '2012-07-02 19:30:00+00:00',
               '2012-07-02 20:00:00+00:00', '2012-07-03 14:30:00+00:00',
               '2012-07-03 15:30:00+00:00', '2012-07-03 16:30:00+00:00',
               '2012-07-03 17:00:00+00:00', '2012-07-05 14:30:00+00:00',
               '2012-07-05 15:30:00+00:00', '2012-07-05 16:30:00+00:00',
               '2012-07-05 17:30:00+00:00', '2012-07-05 18:30:00+00:00',
               '2012-07-05 19:30:00+00:00', '2012-07-05 20:00:00+00:00',
               '2012-07-06 14:30:00+00:00', '2012-07-06 15:30:00+00:00',
               '2012-07-06 16:30:00+00:00', '2012-07-06 17:30:00+00:00',
               '2012-07-06 18:30:00+00:00', '2012-07-06 19:30:00+00:00',
               '2012-07-06 20:00:00+00:00', '2012-07-09 14:30:00+00:00',
               '2012-07-09 15:30:00+00:00', '2012-07-09 16:30:00+00:00',
               '2012-07-09 17:30:00+00:00', '2012-07-09 18:30:00+00:00',
               '2012-07-09 19:30:00+00:00', '2012-07-09 20:00:00+00:00',
               '2012-07-10 14:30:00+00:00', '2012-07-10 15:30:00+00:00',
               '2012-07-10 16:30:00+00:00', '2012-07-10 17:30:00+00:00',
               '2012-07-10 18:30:00+00:00', '2012-07-10 19:30:00+00:00',
               '2012-07-10 20:00:00+00:00'],
              dtype='datetime64[ns, UTC]', freq=None)

Merge schedules

# NYSE Calendar
nyse = mcal.get_calendar('NYSE')
schedule_nyse = nyse.schedule('2015-12-20', '2016-01-06')
schedule_nyse
market_open market_close
2015-12-21 2015-12-21 14:30:00+00:00 2015-12-21 21:00:00+00:00
2015-12-22 2015-12-22 14:30:00+00:00 2015-12-22 21:00:00+00:00
2015-12-23 2015-12-23 14:30:00+00:00 2015-12-23 21:00:00+00:00
2015-12-24 2015-12-24 14:30:00+00:00 2015-12-24 18:00:00+00:00
2015-12-28 2015-12-28 14:30:00+00:00 2015-12-28 21:00:00+00:00
2015-12-29 2015-12-29 14:30:00+00:00 2015-12-29 21:00:00+00:00
2015-12-30 2015-12-30 14:30:00+00:00 2015-12-30 21:00:00+00:00
2015-12-31 2015-12-31 14:30:00+00:00 2015-12-31 21:00:00+00:00
2016-01-04 2016-01-04 14:30:00+00:00 2016-01-04 21:00:00+00:00
2016-01-05 2016-01-05 14:30:00+00:00 2016-01-05 21:00:00+00:00
2016-01-06 2016-01-06 14:30:00+00:00 2016-01-06 21:00:00+00:00
# LSE Calendar
lse = mcal.get_calendar('LSE')
schedule_lse = lse.schedule('2015-12-20', '2016-01-06')
schedule_lse
market_open market_close
2015-12-21 2015-12-21 08:00:00+00:00 2015-12-21 16:30:00+00:00
2015-12-22 2015-12-22 08:00:00+00:00 2015-12-22 16:30:00+00:00
2015-12-23 2015-12-23 08:00:00+00:00 2015-12-23 16:30:00+00:00
2015-12-24 2015-12-24 08:00:00+00:00 2015-12-24 12:30:00+00:00
2015-12-29 2015-12-29 08:00:00+00:00 2015-12-29 16:30:00+00:00
2015-12-30 2015-12-30 08:00:00+00:00 2015-12-30 16:30:00+00:00
2015-12-31 2015-12-31 08:00:00+00:00 2015-12-31 12:30:00+00:00
2016-01-04 2016-01-04 08:00:00+00:00 2016-01-04 16:30:00+00:00
2016-01-05 2016-01-05 08:00:00+00:00 2016-01-05 16:30:00+00:00
2016-01-06 2016-01-06 08:00:00+00:00 2016-01-06 16:30:00+00:00

Inner merge

This will find the dates where both the NYSE and LSE are open. Notice that Dec 28th is open for NYSE but not LSE. Also note that some days have a close prior to the open. This function does not currently check for that.

mcal.merge_schedules(schedules=[schedule_nyse, schedule_lse], how='inner')
market_open market_close
2015-12-21 2015-12-21 14:30:00+00:00 2015-12-21 16:30:00+00:00
2015-12-22 2015-12-22 14:30:00+00:00 2015-12-22 16:30:00+00:00
2015-12-23 2015-12-23 14:30:00+00:00 2015-12-23 16:30:00+00:00
2015-12-24 2015-12-24 14:30:00+00:00 2015-12-24 12:30:00+00:00
2015-12-29 2015-12-29 14:30:00+00:00 2015-12-29 16:30:00+00:00
2015-12-30 2015-12-30 14:30:00+00:00 2015-12-30 16:30:00+00:00
2015-12-31 2015-12-31 14:30:00+00:00 2015-12-31 12:30:00+00:00
2016-01-04 2016-01-04 14:30:00+00:00 2016-01-04 16:30:00+00:00
2016-01-05 2016-01-05 14:30:00+00:00 2016-01-05 16:30:00+00:00
2016-01-06 2016-01-06 14:30:00+00:00 2016-01-06 16:30:00+00:00

Outer merge

This will return the dates and times where either the NYSE or the LSE are open

mcal.merge_schedules(schedules=[schedule_nyse, schedule_lse], how='outer')
market_open market_close
2015-12-21 2015-12-21 08:00:00+00:00 2015-12-21 21:00:00+00:00
2015-12-22 2015-12-22 08:00:00+00:00 2015-12-22 21:00:00+00:00
2015-12-23 2015-12-23 08:00:00+00:00 2015-12-23 21:00:00+00:00
2015-12-24 2015-12-24 08:00:00+00:00 2015-12-24 18:00:00+00:00
2015-12-28 2015-12-28 14:30:00+00:00 2015-12-28 21:00:00+00:00
2015-12-29 2015-12-29 08:00:00+00:00 2015-12-29 21:00:00+00:00
2015-12-30 2015-12-30 08:00:00+00:00 2015-12-30 21:00:00+00:00
2015-12-31 2015-12-31 08:00:00+00:00 2015-12-31 21:00:00+00:00
2016-01-04 2016-01-04 08:00:00+00:00 2016-01-04 21:00:00+00:00
2016-01-05 2016-01-05 08:00:00+00:00 2016-01-05 21:00:00+00:00
2016-01-06 2016-01-06 08:00:00+00:00 2016-01-06 21:00:00+00:00

Use holidays in numpy

This will use your exchange calendar in numpy to add business days

import numpy as np
cme = mcal.get_calendar("CME_Agriculture")
np.busday_offset(dates="2020-05-22", holidays=cme.holidays().holidays, offsets=1)
numpy.datetime64('2020-05-26')

Trading Breaks

Some markets have breaks in the day, like the CME Equity Futures markets which are closed from 4:15 - 4:35 (NY) daily. These calendars will have additional columns in the schedule() DataFrame

cme = mcal.get_calendar('CME_Equity')
schedule = cme.schedule('2020-01-01', '2020-01-04')
schedule
market_open break_start break_end market_close
2020-01-02 2020-01-01 23:00:00+00:00 2020-01-02 21:15:00+00:00 2020-01-02 21:30:00+00:00 2020-01-02 22:00:00+00:00
2020-01-03 2020-01-02 23:00:00+00:00 2020-01-03 21:15:00+00:00 2020-01-03 21:30:00+00:00 2020-01-03 22:00:00+00:00

The date_range() properly accounts for the breaks

mcal.date_range(schedule, '5H')
DatetimeIndex(['2020-01-02 04:00:00+00:00', '2020-01-02 09:00:00+00:00',
               '2020-01-02 14:00:00+00:00', '2020-01-02 19:00:00+00:00',
               '2020-01-02 21:15:00+00:00', '2020-01-02 22:00:00+00:00',
               '2020-01-03 04:00:00+00:00', '2020-01-03 09:00:00+00:00',
               '2020-01-03 14:00:00+00:00', '2020-01-03 19:00:00+00:00',
               '2020-01-03 21:15:00+00:00', '2020-01-03 22:00:00+00:00'],
              dtype='datetime64[ns, UTC]', freq=None)