Skip to content

Commit

Permalink
ENH: Add breaks to TradingCalendar (#154)
Browse files Browse the repository at this point in the history
Co-authored-by: Richard Frank <richafrank@users.noreply.github.com>
  • Loading branch information
gerrymanoim and richafrank authored Oct 9, 2020
1 parent cb5e79f commit 81b1acf
Show file tree
Hide file tree
Showing 7 changed files with 385 additions and 510 deletions.
79 changes: 61 additions & 18 deletions tests/test_trading_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,10 +571,28 @@ def test_minutes_for_period(self):
_open, _close = self.calendar.open_and_close_for_session(
full_session_label
)
_break_start, _break_end = (
self.calendar.break_start_and_end_for_session(
full_session_label
)
)
if not pd.isnull(_break_start):
constructed_minutes = np.concatenate([
pd.date_range(
start=_open, end=_break_start, freq="min"
),
pd.date_range(
start=_break_end, end=_close, freq="min"
)
])
else:
constructed_minutes = pd.date_range(
start=_open, end=_close, freq="min"
)

np.testing.assert_array_equal(
minutes,
pd.date_range(start=_open, end=_close, freq="min")
constructed_minutes,
)

# early close period
Expand Down Expand Up @@ -679,23 +697,48 @@ def test_minutes_in_range(self):
np.testing.assert_array_equal(minutes1, minutes2[1:-1])

# manually construct the minutes
all_minutes = np.concatenate([
pd.date_range(
start=first_open,
end=first_close,
freq="min"
),
pd.date_range(
start=middle_open,
end=middle_close,
freq="min"
),
pd.date_range(
start=last_open,
end=last_close,
freq="min"
)
])
first_break_start, first_break_end = (
self.calendar.break_start_and_end_for_session(sessions[0])
)
middle_break_start, middle_break_end = (
self.calendar.break_start_and_end_for_session(sessions[1])
)
last_break_start, last_break_end = (
self.calendar.break_start_and_end_for_session(sessions[-1])
)

intervals = [
(first_open, first_break_start, first_break_end, first_close),
(middle_open, middle_break_start, middle_break_end, middle_close),
(last_open, last_break_start, last_break_end, last_close),
]
all_minutes = []

for _open, _break_start, _break_end, _close in intervals:
if pd.isnull(_break_start):
all_minutes.append(
pd.date_range(
start=_open,
end=_close,
freq="min"
),
)
else:
all_minutes.append(
pd.date_range(
start=_open,
end=_break_start,
freq="min"
),
)
all_minutes.append(
pd.date_range(
start=_break_end,
end=_close,
freq="min"
),
)
all_minutes = np.concatenate(all_minutes)

np.testing.assert_array_equal(all_minutes, minutes1)

Expand Down
32 changes: 32 additions & 0 deletions tests/test_xhkg_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,38 @@ def test_constrain_construction_dates(self):
),
)

def test_session_break(self):
# Test that the calendar correctly reports itself as closed during
# session break
normal_minute = pd.Timestamp('2003-01-27 03:30:00')
break_minute = pd.Timestamp('2003-01-27 04:30:00')

self.assertTrue(self.calendar.is_open_on_minute(normal_minute))
self.assertFalse(self.calendar.is_open_on_minute(break_minute))
# Make sure that ignoring breaks indicates the exchange is open
self.assertTrue(
self.calendar.is_open_on_minute(break_minute, ignore_breaks=True)
)

current_session_label = self.calendar.minute_to_session_label(
normal_minute,
direction="none"
)
self.assertEqual(
current_session_label,
self.calendar.minute_to_session_label(
break_minute,
direction="previous"
)
)
self.assertEqual(
current_session_label,
self.calendar.minute_to_session_label(
break_minute,
direction="next"
)
)

def test_lunar_new_year_2003(self):
# NOTE: Lunar Month 12 2002 is the 12th month of the lunar year that
# begins in 2002; this month actually takes place in January 2003.
Expand Down
79 changes: 39 additions & 40 deletions trading_calendars/calendar_helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import numpy as np
import pandas as pd

NANOSECONDS_PER_MINUTE = int(6e10)

NP_NAT = np.array([pd.NaT], dtype=np.int64)[0]


def next_divider_idx(dividers, minute_val):

Expand All @@ -25,47 +28,43 @@ def previous_divider_idx(dividers, minute_val):
return divider_idx - 1


def is_open(opens, closes, minute_val):

open_idx = np.searchsorted(opens, minute_val)
close_idx = np.searchsorted(closes, minute_val)

if open_idx != close_idx:
# if the indices are not same, that means the market is open
return True
else:
try:
# if they are the same, it might be the first minute of a
# session
return minute_val == opens[open_idx]
except IndexError:
# this can happen if we're outside the schedule's range (like
# after the last close)
return False


def compute_all_minutes(opens_in_ns, closes_in_ns):
def compute_all_minutes(
opens_in_ns, break_starts_in_ns, break_ends_in_ns, closes_in_ns,
):
"""
Given arrays of opens and closes, both in nanoseconds,
return an array of each minute between the opens and closes.
"""
deltas = closes_in_ns - opens_in_ns

# + 1 because we want 390 mins per standard day, not 389
daily_sizes = (deltas // NANOSECONDS_PER_MINUTE) + 1
num_minutes = daily_sizes.sum()
Given arrays of opens and closes (in nanoseconds) and optionally
break_starts and break ends, return an array of each minute between the
opens and closes.
# One allocation for the entire thing. This assumes that each day
# represents a contiguous block of minutes.
NOTE: Add an extra minute to ending boundaries (break_start and close)
so we include the last bar (arange doesn't include its stop).
"""
pieces = []

for open_, size in zip(opens_in_ns, daily_sizes):
pieces.append(
np.arange(open_,
open_ + size * NANOSECONDS_PER_MINUTE,
NANOSECONDS_PER_MINUTE)
)

out = np.concatenate(pieces).view('datetime64[ns]')
assert len(out) == num_minutes
for open_time, break_start_time, break_end_time, close_time in zip(
opens_in_ns, break_starts_in_ns, break_ends_in_ns, closes_in_ns
):
if break_start_time != NP_NAT:
pieces.append(
np.arange(
open_time,
break_start_time + NANOSECONDS_PER_MINUTE,
NANOSECONDS_PER_MINUTE,
)
)
pieces.append(
np.arange(
break_end_time,
close_time + NANOSECONDS_PER_MINUTE,
NANOSECONDS_PER_MINUTE,
)
)
else:
pieces.append(
np.arange(
open_time,
close_time + NANOSECONDS_PER_MINUTE,
NANOSECONDS_PER_MINUTE,
)
)
out = np.concatenate(pieces).view("datetime64[ns]")
return out
7 changes: 7 additions & 0 deletions trading_calendars/exchange_calendar_xhkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ class XHKGExchangeCalendar(TradingCalendar):
Exchange calendar for the Hong Kong Stock Exchange (XHKG).
Open Time: 9:31 AM, Asia/Hong_Kong
Lunch Break: 12:01 PM - 1:00 PM Asia/Hong_Kong
Close Time: 4:00 PM, Asia/Hong_Kong
Regularly-Observed Holidays:
Expand Down Expand Up @@ -279,6 +280,12 @@ class XHKGExchangeCalendar(TradingCalendar):
(None, time(10, 1)),
(pd.Timestamp('2011-03-07'), time(9, 31)),
)
break_start_times = (
(None, time(12, 1)),
)
break_end_times = (
(None, time(13, 0)),
)
close_times = (
(None, time(16)),
)
Expand Down
Loading

0 comments on commit 81b1acf

Please sign in to comment.