diff --git a/pythclient/calendar.py b/pythclient/calendar.py index 3941113..9fbf7df 100644 --- a/pythclient/calendar.py +++ b/pythclient/calendar.py @@ -6,11 +6,12 @@ EQUITY_OPEN = datetime.time(9, 30, 0, tzinfo=NY_TZ) EQUITY_CLOSE = datetime.time(16, 0, 0, tzinfo=NY_TZ) -EQUITY_EARLY_CLOSE = datetime.time(13, 0, 0, tzinfo=NY_TZ) -# EQUITY_HOLIDAYS and EQUITY_EARLY_HOLIDAYS will need to be updated each year +NYSE_EARLY_CLOSE = datetime.time(13, 0, 0, tzinfo=NY_TZ) + +# NYSE_HOLIDAYS and NYSE_EARLY_HOLIDAYS will need to be updated each year # From https://www.nyse.com/markets/hours-calendars -EQUITY_HOLIDAYS = [ +NYSE_HOLIDAYS = [ datetime.datetime(2023, 1, 2, tzinfo=NY_TZ).date(), datetime.datetime(2023, 1, 16, tzinfo=NY_TZ).date(), datetime.datetime(2023, 2, 20, tzinfo=NY_TZ).date(), @@ -22,7 +23,7 @@ datetime.datetime(2023, 11, 23, tzinfo=NY_TZ).date(), datetime.datetime(2023, 12, 25, tzinfo=NY_TZ).date(), ] -EQUITY_EARLY_HOLIDAYS = [ +NYSE_EARLY_HOLIDAYS = [ datetime.datetime(2023, 7, 3, tzinfo=NY_TZ).date(), datetime.datetime(2023, 11, 24, tzinfo=NY_TZ).date(), ] @@ -36,6 +37,9 @@ datetime.datetime(2023, 12, 25, tzinfo=NY_TZ).date(), ] +RATES_OPEN = datetime.time(8, 0, 0, tzinfo=NY_TZ) +RATES_CLOSE = datetime.time(17, 0, 0, tzinfo=NY_TZ) + def is_market_open(asset_type: str, dt: datetime.datetime) -> bool: # make sure time is in NY timezone @@ -43,11 +47,11 @@ def is_market_open(asset_type: str, dt: datetime.datetime) -> bool: day, date, time = dt.weekday(), dt.date(), dt.time() if asset_type == "equity": - if date in EQUITY_HOLIDAYS or date in EQUITY_EARLY_HOLIDAYS: + if date in NYSE_HOLIDAYS or date in NYSE_EARLY_HOLIDAYS: if ( - date in EQUITY_EARLY_HOLIDAYS + date in NYSE_EARLY_HOLIDAYS and time >= EQUITY_OPEN - and time < EQUITY_EARLY_CLOSE + and time < NYSE_EARLY_CLOSE ): return True return False @@ -70,6 +74,19 @@ def is_market_open(asset_type: str, dt: datetime.datetime) -> bool: return True + if asset_type == "rates": + if date in NYSE_HOLIDAYS or date in NYSE_EARLY_HOLIDAYS: + if ( + date in NYSE_EARLY_HOLIDAYS + and time >= RATES_OPEN + and time < NYSE_EARLY_CLOSE + ): + return True + return False + if day < 5 and time >= RATES_OPEN and time < RATES_CLOSE: + return True + return False + # all other markets (crypto) return True @@ -112,6 +129,22 @@ def get_next_market_open(asset_type: str, dt: datetime.datetime) -> int: ) while is_market_open(asset_type, next_market_open): next_market_open += datetime.timedelta(days=1) + elif asset_type == "rates": + if time < RATES_OPEN: + next_market_open = dt.replace( + hour=RATES_OPEN.hour, + minute=RATES_OPEN.minute, + second=0, + microsecond=0, + ) + else: + next_market_open = dt.replace( + hour=RATES_OPEN.hour, + minute=RATES_OPEN.minute, + second=0, + microsecond=0, + ) + next_market_open += datetime.timedelta(days=1) else: return None @@ -127,11 +160,11 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> int: time = dt.time() if asset_type == "equity": - if dt.date() in EQUITY_EARLY_HOLIDAYS: - if time < EQUITY_EARLY_CLOSE: + if dt.date() in NYSE_EARLY_HOLIDAYS: + if time < NYSE_EARLY_CLOSE: next_market_close = dt.replace( - hour=EQUITY_EARLY_CLOSE.hour, - minute=EQUITY_EARLY_CLOSE.minute, + hour=NYSE_EARLY_CLOSE.hour, + minute=NYSE_EARLY_CLOSE.minute, second=0, microsecond=0, ) @@ -143,20 +176,35 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> int: microsecond=0, ) next_market_close += datetime.timedelta(days=1) - elif dt.date() in EQUITY_HOLIDAYS: - next_market_open = get_next_market_open( - asset_type, dt + datetime.timedelta(days=1) - ) - next_market_close = ( + elif dt.date() in NYSE_HOLIDAYS: + next_market_open = get_next_market_open(asset_type, dt) + next_market_open_date = ( datetime.datetime.fromtimestamp(next_market_open) .astimezone(NY_TZ) - .replace( - hour=EQUITY_CLOSE.hour, - minute=EQUITY_CLOSE.minute, - second=0, - microsecond=0, - ) + .date() ) + if next_market_open_date in NYSE_EARLY_HOLIDAYS: + next_market_close = ( + datetime.datetime.fromtimestamp(next_market_open) + .astimezone(NY_TZ) + .replace( + hour=NYSE_EARLY_CLOSE.hour, + minute=NYSE_EARLY_CLOSE.minute, + second=0, + microsecond=0, + ) + ) + else: + next_market_close = ( + datetime.datetime.fromtimestamp(next_market_open) + .astimezone(NY_TZ) + .replace( + hour=EQUITY_CLOSE.hour, + minute=EQUITY_CLOSE.minute, + second=0, + microsecond=0, + ) + ) else: next_market_close = dt.replace( hour=EQUITY_CLOSE.hour, @@ -167,9 +215,9 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> int: if time >= EQUITY_CLOSE: next_market_close += datetime.timedelta(days=1) - # while next_market_close.date() is in EQUITY_HOLIDAYS or weekend, add 1 day + # while next_market_close.date() is in NYSE_HOLIDAYS or weekend, add 1 day while ( - next_market_close.date() in EQUITY_HOLIDAYS + next_market_close.date() in NYSE_HOLIDAYS or next_market_close.weekday() >= 5 ): next_market_close += datetime.timedelta(days=1) @@ -185,6 +233,68 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> int: next_market_close += datetime.timedelta(days=1) while is_market_open(asset_type, next_market_close): next_market_close += datetime.timedelta(days=1) + elif asset_type == "rates": + if dt.date() in NYSE_EARLY_HOLIDAYS: + if time < NYSE_EARLY_CLOSE: + next_market_close = dt.replace( + hour=NYSE_EARLY_CLOSE.hour, + minute=NYSE_EARLY_CLOSE.minute, + second=0, + microsecond=0, + ) + else: + next_market_close = dt.replace( + hour=RATES_CLOSE.hour, + minute=RATES_CLOSE.minute, + second=0, + microsecond=0, + ) + next_market_close += datetime.timedelta(days=1) + elif dt.date() in NYSE_HOLIDAYS: + next_market_open = get_next_market_open(asset_type, dt) + next_market_open_date = ( + datetime.datetime.fromtimestamp(next_market_open) + .astimezone(NY_TZ) + .date() + ) + if next_market_open_date in NYSE_EARLY_HOLIDAYS: + next_market_close = ( + datetime.datetime.fromtimestamp(next_market_open) + .astimezone(NY_TZ) + .replace( + hour=NYSE_EARLY_CLOSE.hour, + minute=NYSE_EARLY_CLOSE.minute, + second=0, + microsecond=0, + ) + ) + else: + next_market_close = ( + datetime.datetime.fromtimestamp(next_market_open) + .astimezone(NY_TZ) + .replace( + hour=RATES_CLOSE.hour, + minute=RATES_CLOSE.minute, + second=0, + microsecond=0, + ) + ) + else: + next_market_close = dt.replace( + hour=RATES_CLOSE.hour, + minute=RATES_CLOSE.minute, + second=0, + microsecond=0, + ) + if time >= RATES_CLOSE: + next_market_close += datetime.timedelta(days=1) + + # while next_market_close.date() is in NYSE_HOLIDAYS or weekend, add 1 day + while ( + next_market_close.date() in NYSE_HOLIDAYS + or next_market_close.weekday() >= 5 + ): + next_market_close += datetime.timedelta(days=1) else: # crypto markets never close return None diff --git a/setup.py b/setup.py index fc36bff..29350c8 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pythclient', - version='0.1.15', + version='0.1.16', packages=['pythclient'], author='Pyth Developers', author_email='contact@pyth.network', diff --git a/tests/test_calendar.py b/tests/test_calendar.py index de79857..413bbc0 100644 --- a/tests/test_calendar.py +++ b/tests/test_calendar.py @@ -12,7 +12,9 @@ EQUITY_CLOSE_WED_2023_6_21_17 = datetime.datetime(2023, 6, 21, 17, 0, 0, tzinfo=NY_TZ) EQUITY_CLOSE_SAT_2023_6_10_17 = datetime.datetime(2023, 6, 10, 17, 0, 0, tzinfo=NY_TZ) EQUITY_HOLIDAY_MON_2023_6_19 = datetime.datetime(2023, 6, 19, tzinfo=NY_TZ) -EQUITY_EARLY_CLOSE_OPEN_FRI_2023_11_24_14 = datetime.datetime(2023, 11, 24, 11, 0, 0, tzinfo=NY_TZ) +EQUITY_HOLIDAY_NEXT_DAY_EARLY_CLOSE_OPEN_THU_2023_11_23_9_30 = datetime.datetime(2023, 11, 23, 9, 30, 0, tzinfo=NY_TZ) +EQUITY_HOLIDAY_NEXT_DAY_EARLY_CLOSE_CLOSE_THU_2023_11_23_13 = datetime.datetime(2023, 11, 23, 13, 0, 0, tzinfo=NY_TZ) +EQUITY_EARLY_CLOSE_OPEN_FRI_2023_11_24_11 = datetime.datetime(2023, 11, 24, 11, 0, 0, tzinfo=NY_TZ) EQUITY_EARLY_CLOSE_CLOSE_FRI_2023_11_24_14 = datetime.datetime(2023, 11, 24, 14, 0, 0, tzinfo=NY_TZ) # Define constants for fx & metal market @@ -21,6 +23,16 @@ FX_METAL_CLOSE_SUN_2023_6_18_16 = datetime.datetime(2023, 6, 18, 16, 0, 0, tzinfo=NY_TZ) FX_METAL_HOLIDAY_SUN_2023_1_1 = datetime.datetime(2023, 1, 1, tzinfo=NY_TZ) +# Define constants for rates market +RATES_OPEN_WED_2023_6_21_12 = datetime.datetime(2023, 6, 21, 8, 0, 0, tzinfo=NY_TZ) +RATES_CLOSE_WED_2023_6_21_17 = datetime.datetime(2023, 6, 21, 17, 0, 0, tzinfo=NY_TZ) +RATES_CLOSE_SAT_2023_6_10_17 = datetime.datetime(2023, 6, 10, 17, 0, 0, tzinfo=NY_TZ) +RATES_HOLIDAY_MON_2023_6_19 = datetime.datetime(2023, 6, 19, tzinfo=NY_TZ) +RATES_HOLIDAY_NEXT_DAY_EARLY_CLOSE_OPEN_THU_2023_11_23_8 = datetime.datetime(2023, 11, 23, 8, 0, 0, tzinfo=NY_TZ) +RATES_HOLIDAY_NEXT_DAY_EARLY_CLOSE_CLOSE_THU_2023_11_23_13 = datetime.datetime(2023, 11, 23, 13, 0, 0, tzinfo=NY_TZ) +RATES_EARLY_CLOSE_OPEN_FRI_2023_11_24_11 = datetime.datetime(2023, 11, 24, 11, 0, 0, tzinfo=NY_TZ) +RATES_EARLY_CLOSE_CLOSE_FRI_2023_11_24_14 = datetime.datetime(2023, 11, 24, 14, 0, 0, tzinfo=NY_TZ) + # Define constants for cryptocurrency market CRYPTO_OPEN_WED_2023_6_21_12 = datetime.datetime(2023, 6, 21, 12, 0, 0, tzinfo=NY_TZ) CRYPTO_OPEN_SUN_2023_6_18_12 = datetime.datetime(2023, 6, 18, 12, 0, 0, tzinfo=NY_TZ) @@ -47,7 +59,7 @@ def test_is_market_open(): assert is_market_open("equity", EQUITY_HOLIDAY_MON_2023_6_19) == False # weekday, NYSE early close holiday - assert is_market_open("equity", EQUITY_EARLY_CLOSE_OPEN_FRI_2023_11_24_14) == True + assert is_market_open("equity", EQUITY_EARLY_CLOSE_OPEN_FRI_2023_11_24_11) == True assert is_market_open("equity", EQUITY_EARLY_CLOSE_CLOSE_FRI_2023_11_24_14) == False # fx & metal @@ -63,6 +75,23 @@ def test_is_market_open(): assert is_market_open("fx", FX_METAL_HOLIDAY_SUN_2023_1_1) == False assert is_market_open("metal", FX_METAL_HOLIDAY_SUN_2023_1_1) == False + # rates + # weekday, within rates market hours + assert is_market_open("rates", RATES_OPEN_WED_2023_6_21_12) == True + + # weekday, out of rates market hours + assert is_market_open("rates", RATES_CLOSE_WED_2023_6_21_17) == False + + # weekend, out of rates market hours + assert is_market_open("rates", RATES_CLOSE_SAT_2023_6_10_17) == False + + # weekday, NYSE holiday + assert is_market_open("rates", RATES_HOLIDAY_MON_2023_6_19) == False + + # weekday, NYSE early close holiday + assert is_market_open("rates", RATES_EARLY_CLOSE_OPEN_FRI_2023_11_24_11) == True + assert is_market_open("rates", RATES_EARLY_CLOSE_CLOSE_FRI_2023_11_24_14) == False + # crypto assert is_market_open("crypto", CRYPTO_OPEN_WED_2023_6_21_12) == True assert is_market_open("crypto", CRYPTO_OPEN_SUN_2023_6_18_12) == True @@ -93,9 +122,15 @@ def test_get_next_market_open(): == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 20, 9, 30, 0, tzinfo=NY_TZ)) ) + # equity holiday next day early close holiday + assert ( + get_next_market_open("equity", EQUITY_HOLIDAY_NEXT_DAY_EARLY_CLOSE_OPEN_THU_2023_11_23_9_30) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 24, 9, 30, 0, tzinfo=NY_TZ)) + ) + # equity early close holiday assert ( - get_next_market_open("equity", EQUITY_EARLY_CLOSE_OPEN_FRI_2023_11_24_14) + get_next_market_open("equity", EQUITY_EARLY_CLOSE_OPEN_FRI_2023_11_24_11) == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 27, 9, 30, 0, tzinfo=NY_TZ)) ) assert ( @@ -142,6 +177,47 @@ def test_get_next_market_open(): == format_datetime_to_unix_timestamp(datetime.datetime(2023, 1, 2, 17, 0, 0, tzinfo=NY_TZ)) ) + # rates within market hours + assert ( + get_next_market_open("rates", RATES_OPEN_WED_2023_6_21_12) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 22, 8, 0, 0, tzinfo=NY_TZ)) + ) + + # rates out of market hours + assert ( + get_next_market_open("rates", RATES_CLOSE_WED_2023_6_21_17) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 22, 8, 0, 0, tzinfo=NY_TZ)) + ) + + # rates weekend + assert ( + get_next_market_open("rates", RATES_CLOSE_SAT_2023_6_10_17) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 12, 8, 0, 0, tzinfo=NY_TZ)) + ) + + # rates holiday + assert ( + get_next_market_open("rates", RATES_HOLIDAY_MON_2023_6_19) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 20, 8, 0, 0, tzinfo=NY_TZ)) + ) + + # rates holiday next day early close holiday + assert ( + get_next_market_open("rates", RATES_HOLIDAY_NEXT_DAY_EARLY_CLOSE_OPEN_THU_2023_11_23_8) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 24, 8, 0, 0, tzinfo=NY_TZ)) + ) + + # rates early close holiday + assert ( + get_next_market_open("rates", RATES_EARLY_CLOSE_OPEN_FRI_2023_11_24_11) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 27, 8, 0, 0, tzinfo=NY_TZ)) + ) + assert ( + get_next_market_open("rates", RATES_EARLY_CLOSE_CLOSE_FRI_2023_11_24_14) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 27, 8, 0, 0, tzinfo=NY_TZ)) + ) + + # crypto assert get_next_market_open("crypto", CRYPTO_OPEN_WED_2023_6_21_12) == None assert get_next_market_open("crypto", CRYPTO_OPEN_SUN_2023_6_18_12) == None @@ -172,9 +248,15 @@ def test_get_next_market_close(): == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 20, 16, 0, 0, tzinfo=NY_TZ)) ) + # equity holiday next day early close holiday + assert ( + get_next_market_close("equity", EQUITY_HOLIDAY_NEXT_DAY_EARLY_CLOSE_CLOSE_THU_2023_11_23_13) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 24, 13, 0, 0, tzinfo=NY_TZ)) + ) + # equity early close holiday assert ( - get_next_market_close("equity", EQUITY_EARLY_CLOSE_OPEN_FRI_2023_11_24_14) + get_next_market_close("equity", EQUITY_EARLY_CLOSE_OPEN_FRI_2023_11_24_11) == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 24, 13, 0, 0, tzinfo=NY_TZ)) ) assert ( @@ -222,6 +304,46 @@ def test_get_next_market_close(): == format_datetime_to_unix_timestamp(datetime.datetime(2023, 1, 6, 17, 0, 0, tzinfo=NY_TZ)) ) + # rates within market hours + assert ( + get_next_market_close("rates", RATES_OPEN_WED_2023_6_21_12) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 21, 17, 0, 0, tzinfo=NY_TZ)) + ) + + # rates out of market hours + assert ( + get_next_market_close("rates", RATES_CLOSE_WED_2023_6_21_17) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 22, 17, 0, 0, tzinfo=NY_TZ)) + ) + + # rates weekend + assert ( + get_next_market_close("rates", RATES_CLOSE_SAT_2023_6_10_17) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 12, 17, 0, 0, tzinfo=NY_TZ)) + ) + + # rates holiday + assert ( + get_next_market_close("rates", RATES_HOLIDAY_MON_2023_6_19) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 20, 17, 0, 0, tzinfo=NY_TZ)) + ) + + # rates holiday next day early close holiday + assert ( + get_next_market_close("rates", RATES_HOLIDAY_NEXT_DAY_EARLY_CLOSE_CLOSE_THU_2023_11_23_13) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 24, 13, 0, 0, tzinfo=NY_TZ)) + ) + + # rates early close holiday + assert ( + get_next_market_close("rates", RATES_EARLY_CLOSE_OPEN_FRI_2023_11_24_11) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 24, 13, 0, 0, tzinfo=NY_TZ)) + ) + assert ( + get_next_market_close("rates", RATES_EARLY_CLOSE_CLOSE_FRI_2023_11_24_14) + == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 27, 17, 0, 0, tzinfo=NY_TZ)) + ) + # crypto assert get_next_market_close("crypto", CRYPTO_OPEN_WED_2023_6_21_12) == None assert get_next_market_close("crypto", CRYPTO_OPEN_SUN_2023_6_18_12) == None