diff --git a/pyintegration/deephaven2/calendar.py b/pyintegration/deephaven2/calendar.py new file mode 100644 index 00000000000..498f85f33fb --- /dev/null +++ b/pyintegration/deephaven2/calendar.py @@ -0,0 +1,509 @@ +# +# Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending +# +""" This module provides a collection of business calendars and convenient calendar functions. """ + +from enum import Enum +from typing import List + +import jpy +from deephaven2.time import TimeZone, DateTime + +from deephaven2 import DHError +from deephaven2._wrapper_abc import JObjectWrapper + +_JCalendars = jpy.get_type("io.deephaven.time.calendar.Calendars") +_JCalendar = jpy.get_type("io.deephaven.time.calendar.Calendar") +_JDayOfWeek = jpy.get_type("java.time.DayOfWeek") +_JBusinessPeriod = jpy.get_type("io.deephaven.time.calendar.BusinessPeriod") +_JBusinessSchedule = jpy.get_type("io.deephaven.time.calendar.BusinessSchedule") +_JBusinessCalendar = jpy.get_type("io.deephaven.time.calendar.BusinessCalendar") + + +def calendar_names() -> List[str]: + """ Returns the names of all available calendars. + + Returns: + a list of names of all available calendars + + Raises: + DHError + """ + try: + return list(_JCalendars.calendarNames()) + except Exception as e: + raise DHError(e, "failed to obtain the available calendar names.") from e + + +def default_calendar_name(): + """ Returns the default calendar name which is set by the 'Calendar.default' property in the configuration file + that the Deephaven server is started with. + + Returns: + the default business calendar name + + Raises: + DHError + """ + try: + return _JCalendars.getDefaultName() + except Exception as e: + raise DHError(e, "failed to get the default calendar name.") from e + + +class DayOfWeek(Enum): + """ A Enum that defines the days of a week. """ + MONDAY = _JDayOfWeek.MONDAY + TUESDAY = _JDayOfWeek.TUESDAY + WEDNESDAY = _JDayOfWeek.WEDNESDAY + THURSDAY = _JDayOfWeek.THURSDAY + FRIDAY = _JDayOfWeek.FRIDAY + SATURDAY = _JDayOfWeek.SATURDAY + SUNDAY = _JDayOfWeek.SUNDAY + + +class BusinessPeriod(JObjectWrapper): + """ A period of business time during a business day. """ + + j_object_type = _JBusinessPeriod + + def __init__(self, j_business_period): + self.j_business_period = j_business_period + + def __repr__(self): + return f"[{self.start_time}, {self.end_time}]" + + @property + def j_object(self) -> jpy.JType: + return self.j_business_period + + @property + def start_time(self) -> DateTime: + """ The start of the period. """ + return self.j_business_period.getStartTime() + + @property + def end_time(self) -> DateTime: + """ The end of the period. """ + return self.j_business_period.getEndTime() + + @property + def length(self) -> int: + """ The length of the period in nanoseconds. """ + return self.j_business_period.getLength() + + +class BusinessSchedule(JObjectWrapper): + """ A business schedule defines a single business day. """ + j_object_type = _JBusinessSchedule + + def __init__(self, j_business_schedule): + self.j_business_schedule = j_business_schedule + + @property + def j_object(self) -> jpy.JType: + return self.j_business_schedule + + @property + def business_periods(self) -> List[BusinessPeriod]: + """ The business periods of the day. + + Returns: + List[BusinessPeriod] + """ + j_periods = self.j_business_schedule.getBusinessPeriods() + periods = [] + for j_period in j_periods: + periods.append(BusinessPeriod(j_period)) + return periods + + @property + def start_of_day(self) -> DateTime: + """ The start of the business day. """ + return self.j_business_schedule.getStartOfBusinessDay() + + @property + def end_of_day(self) -> DateTime: + """ The end of the business day. """ + return self.j_business_schedule.getEndOfBusinessDay() + + def is_business_day(self) -> bool: + """ Whether it is a business day. + + Returns: + bool + """ + return self.j_business_schedule.isBusinessDay() + + def is_business_time(self, time: DateTime) -> bool: + """ Whether the specified time is a business time for the day. + + Args: + time (DateTime): the time during the day + + Return: + bool + """ + return self.j_business_schedule.isBusinessTime(time) + + def business_time_elapsed(self, time: DateTime) -> int: + """ Returns the amount of business time in nanoseconds that has elapsed on the given day by the specified + time. + + Args: + time (DateTime): the time during the day + + Returns: + int + """ + return self.j_business_schedule.businessTimeElapsed(time) + + +class Calendar(JObjectWrapper): + j_object_type = _JCalendar + + @property + def j_object(self) -> jpy.JType: + return self.j_calendar + + @property + def current_day(self) -> str: + """ The current day. """ + return self.j_calendar.currentDay() + + @property + def time_zone(self) -> TimeZone: + """ Returns the timezone of the calendar. """ + return TimeZone(self.j_calendar.timeZone()) + + def previous_day(self, date: str) -> str: + """ Gets the day prior to the given date. + + Args: + date (str): the date of interest + + Returns: + str + + Raises: + DHError + """ + try: + return self.j_calendar.previousDay(date) + except Exception as e: + raise DHError(e, "failed in previous_day.") from e + + def next_day(self, date: str) -> str: + """ Gets the day after the given date. + + Args: + date (str): the date of interest + + Returns: + str + + Raises: + DHError + """ + try: + return self.j_calendar.nextDay(date) + except Exception as e: + raise DHError(e, "failed in next_day.") from e + + def day_of_week(self, date: str) -> DayOfWeek: + """ The day of week for the given date. + + Args: + date (str): the date of interest + + Returns: + str + + Raises: + DHError + """ + try: + return DayOfWeek(self.j_calendar.dayOfWeek(date)) + except Exception as e: + raise DHError(e, "failed in day_of_week.") from e + + def days_in_range(self, start: str, end: str) -> List[str]: + """ Returns the days between the specified start and end dates. + + Args: + start (str): the start day of the range + end (str): the end day of the range + + Returns: + List[str]: a list of dates + + Raises: + DHError + """ + try: + j_days = self.j_calendar.daysInRange(start, end) + return list(j_days) + except Exception as e: + raise DHError(e, "failed in days_in_range.") from e + + def number_of_days(self, start: str, end: str, end_inclusive: bool = False) -> int: + """ Returns the number of days between the start and end dates. + + Args: + start (str): the start day of the range + end (str): the end day of the range + end_inclusive (bool): whether to include the end date, default is False + + Returns: + int + + Raises: + DHError + """ + try: + return self.j_calendar.numberOfDays(start, end, end_inclusive) + except Exception as e: + raise DHError(e, "failed in number_of_days.") from e + + +class BusinessCalendar(Calendar): + """ A business calendar. """ + + j_object_type = _JBusinessCalendar + + def __init__(self, name: str = None): + """ Loads a business calendar. + + Args: + name (str) : name of the calendar, default is None, which means to use the default Deephaven calendar + + Raises: + DHError + """ + try: + if name: + self.name = name + self.j_calendar = _JCalendars.calendar(name) + else: + self.name = default_calendar_name() + self.j_calendar = _JCalendars.calendar() + except Exception as e: + raise DHError(e, "failed to load a business calendar.") from e + + @property + def is_business_day(self) -> bool: + """ If today is a business day. """ + return self.j_calendar.isBusinessDay() + + @property + def default_business_periods(self) -> List[str]: + """ The default business periods for the business days. Returns a list of strings with a comma separating + open and close times. """ + + default_business_periods = self.j_calendar.getDefaultBusinessPeriods() + b_periods = [] + for i in range(default_business_periods.size()): + p = default_business_periods.get(i) + b_periods.append(p) + + return b_periods + + @property + def standard_business_day_length(self) -> int: + """ The length of a standard business day in nanoseconds. """ + return self.j_calendar.standardBusinessDayLengthNanos() + + def business_schedule(self, date: str) -> BusinessSchedule: + """ Returns the specified day's business schedule. + + Args: + date (str): the date str, format must be "yyyy-MM-dd" + + Returns: + a BusinessSchedule instance + + Raises: + DHError + """ + try: + return BusinessSchedule(j_business_schedule=self.j_calendar.getBusinessSchedule(date)) + except Exception as e: + raise DHError(e, "failed in get_business_schedule.") from e + + def previous_business_day(self, date: str) -> str: + """ Gets the business day prior to the given date. + + Args: + date (str): the date of interest + + Returns: + str + + Raises: + DHError + """ + try: + return self.j_calendar.previousBusinessDay(date) + except Exception as e: + raise DHError(e, "failed in previous_business_day.") from e + + def previous_non_business_day(self, date: str) -> str: + """ Gets the non-business day prior to the given date. + + Args: + date (str): the date of interest + + Returns: + str + + Raises: + DHError + """ + try: + return self.j_calendar.previousNonBusinessDay(date) + except Exception as e: + raise DHError(e, "failed in previous_non_business_day.") from e + + def next_business_day(self, date: str) -> str: + """ Gets the business day after the given date. + + Args: + date (str): the date of interest + + Returns: + str + + Raises: + DHError + """ + try: + return self.j_calendar.nextBusinessDay(date) + except Exception as e: + raise DHError(e, "failed in next_business_day.") from e + + def next_non_business_day(self, date: str) -> str: + """ Gets the non-business day after the given date. + + Args: + date (str): the date of interest + + Returns: + str + + Raises: + DHError + """ + try: + return self.j_calendar.nextNonBusinessDay(date) + except Exception as e: + raise DHError(e, "failed in next_non_business_day.") from e + + def business_days_in_range(self, start: str, end: str) -> List[str]: + """ Returns the business days between the specified start and end dates. + + Args: + start (str): the start day of the range + end (str): the end day of the range + + Returns: + List[str]: a list of dates + + Raises: + DHError + """ + try: + j_days = self.j_calendar.businessDaysInRange(start, end) + return list(j_days) + except Exception as e: + raise DHError(e, "failed in business_days_in_range.") from e + + def non_business_days_in_range(self, start: str, end: str) -> List[str]: + """ Returns the non-business days between the specified start and end dates. + + Args: + start (str): the start day of the range + end (str): the end day of the range + + Returns: + List[str]: a list of dates + + Raises: + DHError + """ + try: + j_days = self.j_calendar.nonBusinessDaysInRange(start, end) + return list(j_days) + except Exception as e: + raise DHError(e, "failed in non_business_days_in_range.") from e + + def number_of_business_days(self, start: str, end: str, end_inclusive: bool = False) -> int: + """ Returns the number of business days between the start and end dates. + + Args: + start (str): the start day of the range + end (str): the end day of the range + end_inclusive (bool): whether to include the end date, default is False + + Returns: + int + + Raises: + DHError + """ + try: + return self.j_calendar.numberOfBusinessDays(start, end, end_inclusive) + except Exception as e: + raise DHError(e, "failed in number_of_business_days.") from e + + def number_of_non_business_days(self, start: str, end: str, end_inclusive: bool = False) -> int: + """ Returns the number of non-business days between the start and end dates. + + Args: + start (str): the start day of the range + end (str): the end day of the range + end_inclusive (bool): whether to include the end date, default is False + + Returns: + int + + Raises: + DHError + """ + try: + return self.j_calendar.numberOfNonBusinessDays(start, end, end_inclusive) + except Exception as e: + raise DHError(e, "failed in number_of_non_business_days.") from e + + def is_last_business_day_of_month(self, date: str) -> bool: + """ Returns if the specified date is the last business day of the month. + + Args: + date (str): the date + + Returns: + bool + + Raises: + DHError + """ + try: + return self.j_calendar.isLastBusinessDayOfMonth(date) + except Exception as e: + raise DHError(e, "failed in is_last_business_day_of_month.") from e + + def is_last_business_day_of_week(self, date: str) -> bool: + """ Returns if the specified date is the last business day of the week. + + Args: + date (str): the date + + Returns: + bool + + Raises: + DHError + """ + try: + return self.j_calendar.isLastBusinessDayOfWeek(date) + except Exception as e: + raise DHError(e, "failed in is_last_business_day_of_week.") from e diff --git a/pyintegration/deephaven2/time.py b/pyintegration/deephaven2/time.py index 3a24d70648e..14a76d11a55 100644 --- a/pyintegration/deephaven2/time.py +++ b/pyintegration/deephaven2/time.py @@ -162,8 +162,8 @@ def datetime_at_midnight(dt: DateTime, tz: TimeZone) -> DateTime: """ Returns a DateTime for the requested DateTime at midnight in the specified time zone. Args: - dt (DateTime) - DateTime for which the new value at midnight should be calculated - tz: (TimeZone) - TimeZone for which the new value at midnight should be calculated + dt (DateTime): the DateTime for which the new value at midnight should be calculated + tz (TimeZone): the TimeZone to use when interpreting the DateTime Returns: DateTime diff --git a/pyintegration/tests/test_calendar.py b/pyintegration/tests/test_calendar.py new file mode 100644 index 00000000000..a53172a551d --- /dev/null +++ b/pyintegration/tests/test_calendar.py @@ -0,0 +1,136 @@ +# +# Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending +# + +import unittest + +from deephaven2 import DHError, time +from deephaven2.calendar import calendar_names, default_calendar_name, BusinessCalendar, DayOfWeek +from deephaven2.config import get_server_timezone +from tests.testbase import BaseTestCase + + +class CalendarTestCase(BaseTestCase): + def setUp(self) -> None: + self.test_calendar = BusinessCalendar("USNYSE") + self.b_day1 = "2022-01-03" + self.b_day = "2022-03-08" + self.prev_b_day = "2022-03-07" + self.prev_nb_day = "2022-03-06" + self.next_b_day = "2022-03-09" + self.next_nb_day = "2022-03-12" + self.last_b_day_month = "2022-03-31" + self.last_b_day_week = "2022-03-11" + + def test_calendar(self): + with self.subTest(msg="calendarNames() test"): + cal_names = calendar_names() + self.assertTrue(isinstance(cal_names, list) and len(cal_names) > 0) + + with self.subTest(msg="getDefaultName() test"): + def_cal_name = default_calendar_name() + self.assertIsNotNone(def_cal_name) + + with self.subTest(msg="calendar() construction"): + self.assertIsNotNone(BusinessCalendar()) + + with self.subTest(msg="calendar(invalid name) construction"): + self.assertRaises(DHError, BusinessCalendar, "garbage_name") + + with self.subTest(msg="calendar(valid name) construction"): + self.assertIsNotNone(BusinessCalendar(cal_names[0])) + + default_calendar = BusinessCalendar() + current_date = default_calendar.current_day + self.assertIsNotNone(default_calendar.previous_day(current_date)) + self.assertIsNotNone(default_calendar.next_day(current_date)) + self.assertIsNotNone(default_calendar.day_of_week(current_date).name) + self.assertEqual(default_calendar.time_zone, get_server_timezone()) + + self.assertEqual(self.test_calendar.previous_day(self.b_day), self.prev_b_day) + self.assertEqual(self.test_calendar.next_day(self.b_day), self.next_b_day) + self.assertEqual(self.test_calendar.days_in_range(self.prev_nb_day, self.b_day), + [self.prev_nb_day, self.test_calendar.next_day(self.prev_nb_day), self.b_day]) + self.assertEqual(self.test_calendar.number_of_days(self.prev_nb_day, self.next_b_day), 3) + self.assertEqual(self.test_calendar.day_of_week(self.b_day), DayOfWeek.TUESDAY) + + bad_date = "2022-A1-03" + with self.assertRaises(DHError) as cm: + self.assertFalse(self.test_calendar.is_last_business_day_of_month(bad_date)) + + def test_business_period(self): + b_periods = self.test_calendar.business_schedule(self.b_day1).business_periods + self.assertEqual(len(b_periods), 1) + p = b_periods[0] + s = time.format_datetime(p.start_time, time.TimeZone.NY) + self.assertEqual(p.start_time, time.to_datetime(s)) + s = time.format_datetime(p.end_time, time.TimeZone.NY) + self.assertEqual(p.end_time, time.to_datetime(s)) + self.assertEqual(p.length, 6.5 * 60 * 60 * 10 ** 9) + + def test_business_schedule_business_day(self): + b_schedule = self.test_calendar.business_schedule(self.prev_nb_day) + self.assertFalse(b_schedule.is_business_day()) + + b_schedule = self.test_calendar.business_schedule(self.b_day) + self.assertTrue(b_schedule.is_business_day()) + + b_period = b_schedule.business_periods[0] + self.assertEqual(b_period.start_time, b_schedule.start_of_day) + self.assertEqual(b_period.end_time, b_schedule.end_of_day) + + self.assertTrue(b_schedule.is_business_time(b_period.start_time)) + self.assertTrue(b_schedule.is_business_time(b_period.end_time)) + non_b_time = time.minus_nanos(b_schedule.start_of_day, 1) + self.assertFalse(b_schedule.is_business_time(non_b_time)) + non_b_time = time.plus_nanos(b_schedule.end_of_day, 1) + self.assertFalse(b_schedule.is_business_time(non_b_time)) + + b_time = time.plus_nanos(b_schedule.start_of_day, 10 * 10 ** 9) + self.assertEqual(10 * 10 ** 9, b_schedule.business_time_elapsed(b_time)) + b_time = time.plus_nanos(b_schedule.end_of_day, 10 * 10 ** 9) + self.assertEqual(b_period.length, b_schedule.business_time_elapsed(b_time)) + + def test_business_calendar(self): + default_calendar = BusinessCalendar() + + self.assertIn(default_calendar.is_business_day, {True, False}) + self.assertEqual(self.test_calendar.default_business_periods, ["09:30,16:00"]) + self.assertEqual(self.test_calendar.standard_business_day_length, 6.5 * 60 * 60 * 10 ** 9) + self.assertEqual(self.test_calendar.previous_business_day(self.b_day), self.prev_b_day) + self.assertEqual(self.test_calendar.previous_non_business_day(self.b_day), self.prev_nb_day) + self.assertEqual(self.test_calendar.next_business_day(self.b_day), self.next_b_day) + self.assertEqual(self.test_calendar.next_non_business_day(self.b_day), self.next_nb_day) + + self.assertEqual(self.test_calendar.business_days_in_range(self.prev_nb_day, self.b_day), + [self.prev_b_day, self.b_day]) + self.assertEqual(self.test_calendar.non_business_days_in_range(self.prev_nb_day, self.b_day), + [self.prev_nb_day]) + + self.assertEqual(self.test_calendar.number_of_business_days(self.prev_nb_day, self.next_b_day), 2) + self.assertEqual( + self.test_calendar.number_of_business_days(self.prev_nb_day, self.next_b_day, end_inclusive=True), 3) + self.assertEqual(self.test_calendar.number_of_non_business_days(self.prev_nb_day, self.next_nb_day), 1) + self.assertEqual( + self.test_calendar.number_of_non_business_days(self.prev_nb_day, self.next_nb_day, end_inclusive=True), 2) + + self.assertFalse(self.test_calendar.is_last_business_day_of_month(self.b_day)) + self.assertFalse(self.test_calendar.is_last_business_day_of_week(self.b_day)) + self.assertTrue(self.test_calendar.is_last_business_day_of_month(self.last_b_day_month)) + self.assertTrue(self.test_calendar.is_last_business_day_of_week(self.last_b_day_week)) + + bad_date = "2022-A1-03" + with self.assertRaises(DHError) as cm: + self.assertFalse(self.test_calendar.is_last_business_day_of_month(bad_date)) + + self.assertIn("RuntimeError", cm.exception.root_cause) + + def test_business_schedule_non_business_day(self): + default_calendar = BusinessCalendar() + business_schedule = default_calendar.business_schedule(self.next_nb_day) + self.assertEqual(business_schedule.business_periods, []) + self.assertFalse(business_schedule.is_business_day()) + + +if __name__ == '__main__': + unittest.main()