From 10c28a1f2d54eab8759897804c3e1ea5e8b4d539 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Wed, 21 Jun 2023 15:21:44 +0800 Subject: [PATCH 1/6] fix get_next_market_open/close logic --- pythclient/calendar.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pythclient/calendar.py b/pythclient/calendar.py index 2a1588c..b1cdc8b 100644 --- a/pythclient/calendar.py +++ b/pythclient/calendar.py @@ -79,9 +79,6 @@ def get_next_market_open(asset_type: str, dt: datetime.datetime) -> str: dt = dt.astimezone(NY_TZ) time = dt.time() - if is_market_open(asset_type, dt): - return dt.astimezone(UTC_TZ).strftime("%Y-%m-%dT%H:%M:%S") + "Z" - if asset_type == "equity": if time < EQUITY_OPEN: next_market_open = dt.replace( @@ -123,11 +120,10 @@ def get_next_market_open(asset_type: str, dt: datetime.datetime) -> str: return next_market_open.astimezone(UTC_TZ).strftime("%Y-%m-%dT%H:%M:%S") + "Z" + def get_next_market_close(asset_type: str, dt: datetime.datetime) -> str: # make sure time is in NY timezone dt = dt.astimezone(NY_TZ) - if not is_market_open(asset_type, dt): - return dt.astimezone(UTC_TZ).strftime("%Y-%m-%dT%H:%M:%S") + "Z" if asset_type == "equity": if dt.date() in EQUITY_EARLY_HOLIDAYS: @@ -145,6 +141,8 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> str: second=0, microsecond=0, ) + if dt >= next_market_close: + next_market_close += datetime.timedelta(days=1) elif asset_type in ["fx", "metal"]: next_market_close = dt.replace( hour=FX_METAL_OPEN_CLOSE_TIME.hour, @@ -152,7 +150,12 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> str: second=0, microsecond=0, ) + if dt >= next_market_close: + next_market_close += datetime.timedelta(days=1) else: # crypto markets never close return None + while not is_market_open(asset_type, next_market_close): + next_market_close += datetime.timedelta(days=1) + return next_market_close.astimezone(UTC_TZ).strftime("%Y-%m-%dT%H:%M:%S") + "Z" From fa5fbedce167094ad6c07244865752fa975de4a2 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Wed, 21 Jun 2023 15:21:56 +0800 Subject: [PATCH 2/6] bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6445a1a..c44fe2b 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pythclient', - version='0.1.6', + version='0.1.7', packages=['pythclient'], author='Pyth Developers', author_email='contact@pyth.network', From eb7fde5d23d85a8702d5735753579d3e658e0454 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Wed, 21 Jun 2023 15:27:33 +0800 Subject: [PATCH 3/6] crypto return none for next market open --- pythclient/calendar.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pythclient/calendar.py b/pythclient/calendar.py index b1cdc8b..4417aa9 100644 --- a/pythclient/calendar.py +++ b/pythclient/calendar.py @@ -112,8 +112,7 @@ def get_next_market_open(asset_type: str, dt: datetime.datetime) -> str: ) next_market_open += datetime.timedelta(days=1) else: - next_market_open = dt.replace(hour=0, minute=0, second=0, microsecond=0) - next_market_open += datetime.timedelta(days=1) + return None while not is_market_open(asset_type, next_market_open): next_market_open += datetime.timedelta(days=1) @@ -159,3 +158,5 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> str: next_market_close += datetime.timedelta(days=1) return next_market_close.astimezone(UTC_TZ).strftime("%Y-%m-%dT%H:%M:%S") + "Z" + +print(get_next_market_close("equity", datetime.datetime.now())) \ No newline at end of file From 28a33bd629dedfa09ee297f37f785158ab3cc3ac Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Thu, 22 Jun 2023 17:18:04 +0800 Subject: [PATCH 4/6] add unit tests and fix logic --- pythclient/calendar.py | 66 ++++++-- tests/test_calendar.py | 361 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 409 insertions(+), 18 deletions(-) create mode 100644 tests/test_calendar.py diff --git a/pythclient/calendar.py b/pythclient/calendar.py index 4417aa9..3d6ac57 100644 --- a/pythclient/calendar.py +++ b/pythclient/calendar.py @@ -47,11 +47,11 @@ def is_market_open(asset_type: str, dt: datetime.datetime) -> bool: if ( date in EQUITY_EARLY_HOLIDAYS and time >= EQUITY_OPEN - and time <= EQUITY_EARLY_CLOSE + and time < EQUITY_EARLY_CLOSE ): return True return False - if day < 5 and time >= EQUITY_OPEN and time <= EQUITY_CLOSE: + if day < 5 and time >= EQUITY_OPEN and time < EQUITY_CLOSE: return True return False @@ -59,7 +59,7 @@ def is_market_open(asset_type: str, dt: datetime.datetime) -> bool: if date in FX_METAL_HOLIDAYS: return False # On Friday the market is closed after 5pm - if day == 4 and time > FX_METAL_OPEN_CLOSE_TIME: + if day == 4 and time >= FX_METAL_OPEN_CLOSE_TIME: return False # On Saturday the market is closed all the time if day == 5: @@ -110,7 +110,9 @@ def get_next_market_open(asset_type: str, dt: datetime.datetime) -> str: second=0, microsecond=0, ) - next_market_open += datetime.timedelta(days=1) + while is_market_open(asset_type, next_market_open): + next_market_open += datetime.timedelta(days=1) + else: return None @@ -123,15 +125,38 @@ def get_next_market_open(asset_type: str, dt: datetime.datetime) -> str: def get_next_market_close(asset_type: str, dt: datetime.datetime) -> str: # make sure time is in NY timezone dt = dt.astimezone(NY_TZ) + time = dt.time() if asset_type == "equity": if dt.date() in EQUITY_EARLY_HOLIDAYS: - - next_market_close = dt.replace( - hour=EQUITY_EARLY_CLOSE.hour, - minute=EQUITY_EARLY_CLOSE.minute, - second=0, - microsecond=0, + if time < EQUITY_EARLY_CLOSE: + next_market_close = dt.replace( + hour=EQUITY_EARLY_CLOSE.hour, + minute=EQUITY_EARLY_CLOSE.minute, + second=0, + microsecond=0, + ) + else: + next_market_close = dt.replace( + hour=EQUITY_CLOSE.hour, + minute=EQUITY_CLOSE.minute, + second=0, + 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 = ( + datetime.datetime.fromisoformat(next_market_open.replace("Z", "+00:00")) + .astimezone(NY_TZ) + .replace( + hour=EQUITY_CLOSE.hour, + minute=EQUITY_CLOSE.minute, + second=0, + microsecond=0, + ) ) else: next_market_close = dt.replace( @@ -140,8 +165,16 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> str: second=0, microsecond=0, ) - if dt >= next_market_close: + 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() in EQUITY_HOLIDAYS + or next_market_close.weekday() >= 5 + ): next_market_close += datetime.timedelta(days=1) + elif asset_type in ["fx", "metal"]: next_market_close = dt.replace( hour=FX_METAL_OPEN_CLOSE_TIME.hour, @@ -149,14 +182,11 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> str: second=0, microsecond=0, ) - if dt >= next_market_close: + while not is_market_open(asset_type, next_market_close): + next_market_close += datetime.timedelta(days=1) + while is_market_open(asset_type, next_market_close): next_market_close += datetime.timedelta(days=1) - else: # crypto markets never close + else: # crypto markets never close return None - while not is_market_open(asset_type, next_market_close): - next_market_close += datetime.timedelta(days=1) - return next_market_close.astimezone(UTC_TZ).strftime("%Y-%m-%dT%H:%M:%S") + "Z" - -print(get_next_market_close("equity", datetime.datetime.now())) \ No newline at end of file diff --git a/tests/test_calendar.py b/tests/test_calendar.py new file mode 100644 index 0000000..f9bf1a9 --- /dev/null +++ b/tests/test_calendar.py @@ -0,0 +1,361 @@ +import datetime +from zoneinfo import ZoneInfo + +import pytest + +from pythclient.calendar import ( + get_next_market_close, + get_next_market_open, + is_market_open, +) + +NY_TZ = ZoneInfo("America/New_York") +UTC_TZ = ZoneInfo("UTC") + + +@pytest.fixture +def equity_open_weekday_datetime(): + # A weekday, within equity market hours + return datetime.datetime(2023, 6, 21, 12, 0, 0, tzinfo=NY_TZ) + + +@pytest.fixture +def equity_close_weekday_datetime(): + # A weekday, out of equity market hours + return datetime.datetime(2023, 6, 21, 17, 0, 0, tzinfo=NY_TZ) + + +@pytest.fixture +def equity_close_weekend_datetime(): + # A weekend, out of equity market hours + return datetime.datetime(2023, 6, 10, 17, 0, 0, tzinfo=NY_TZ) + + +@pytest.fixture +def equity_holiday_datetime(): + # A weekday, NYSE holiday + return datetime.datetime(2023, 6, 19, tzinfo=NY_TZ) + + +@pytest.fixture +def equity_early_holiday_open_datetime(): + # A weekday, NYSE early close holiday + return datetime.datetime(2023, 11, 24, 11, 0, 0, tzinfo=NY_TZ) + + +@pytest.fixture +def equity_early_holiday_close_datetime(): + # A weekday, NYSE early close holiday + return datetime.datetime(2023, 11, 24, 14, 0, 0, tzinfo=NY_TZ) + + +@pytest.fixture +def fx_metal_open_weekday_datetime(): + # A weekday, within fx & metal market hours + return datetime.datetime(2023, 6, 21, 22, 0, 0, tzinfo=NY_TZ) + + +@pytest.fixture +def fx_metal_close_weekend_datetime(): + # A weekend, out of fx & metal market hours + return datetime.datetime(2023, 6, 18, 16, 0, 0, tzinfo=NY_TZ) + + +@pytest.fixture +def fx_metal_holiday_datetime(): + # CBOE holiday + return datetime.datetime(2023, 1, 1, tzinfo=NY_TZ) + + +@pytest.fixture +def crypto_open_weekday_datetime(): + return datetime.datetime(2023, 6, 21, 12, 0, 0, tzinfo=NY_TZ) + + +@pytest.fixture +def crypto_open_weekend_datetime(): + return datetime.datetime(2023, 6, 18, 12, 0, 0, tzinfo=NY_TZ) + + +def test_is_market_open( + equity_open_weekday_datetime, + equity_close_weekday_datetime, + equity_close_weekend_datetime, + equity_holiday_datetime, + equity_early_holiday_open_datetime, + equity_early_holiday_close_datetime, + fx_metal_open_weekday_datetime, + fx_metal_close_weekend_datetime, + fx_metal_holiday_datetime, + crypto_open_weekday_datetime, + crypto_open_weekend_datetime, +): + # equity + # weekday, within equity market hours + assert is_market_open("equity", equity_open_weekday_datetime) == True + + # weekday, out of equity market hours + assert is_market_open("equity", equity_close_weekday_datetime) == False + + # weekend, out of equity market hours + assert is_market_open("equity", equity_close_weekend_datetime) == False + + # weekday, NYSE holiday + assert is_market_open("equity", equity_holiday_datetime) == False + + # weekday, NYSE early close holiday + assert is_market_open("equity", equity_early_holiday_open_datetime) == True + assert is_market_open("equity", equity_early_holiday_close_datetime) == False + + # fx & metal + # weekday, within fx & metal market hours + assert is_market_open("fx", fx_metal_open_weekday_datetime) == True + assert is_market_open("metal", fx_metal_open_weekday_datetime) == True + + # weekday, out of fx & metal market hours + assert is_market_open("fx", fx_metal_close_weekend_datetime) == False + assert is_market_open("metal", fx_metal_close_weekend_datetime) == False + + # fx & metal holiday + assert is_market_open("fx", fx_metal_holiday_datetime) == False + assert is_market_open("metal", fx_metal_holiday_datetime) == False + + # crypto + assert is_market_open("crypto", crypto_open_weekday_datetime) == True + assert is_market_open("crypto", crypto_open_weekend_datetime) == True + + +def test_get_next_market_open( + equity_open_weekday_datetime, + equity_close_weekday_datetime, + equity_close_weekend_datetime, + equity_holiday_datetime, + equity_early_holiday_open_datetime, + equity_early_holiday_close_datetime, + fx_metal_open_weekday_datetime, + fx_metal_close_weekend_datetime, + fx_metal_holiday_datetime, + crypto_open_weekday_datetime, + crypto_open_weekend_datetime, +): + # equity within market hours + assert ( + get_next_market_open("equity", equity_open_weekday_datetime) + == datetime.datetime(2023, 6, 22, 9, 30, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # equity out of market hours + assert ( + get_next_market_open("equity", equity_close_weekday_datetime) + == datetime.datetime(2023, 6, 22, 9, 30, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # equity weekend + assert ( + get_next_market_open("equity", equity_close_weekend_datetime) + == datetime.datetime(2023, 6, 12, 9, 30, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # equity holiday + assert ( + get_next_market_open("equity", equity_holiday_datetime) + == datetime.datetime(2023, 6, 20, 9, 30, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # equity early close holiday + assert ( + get_next_market_open("equity", equity_early_holiday_open_datetime) + == datetime.datetime(2023, 11, 27, 9, 30, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + assert ( + get_next_market_open("equity", equity_early_holiday_close_datetime) + == datetime.datetime(2023, 11, 27, 9, 30, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # fx & metal within market hours + assert ( + get_next_market_open("fx", fx_metal_open_weekday_datetime) + == datetime.datetime(2023, 6, 25, 17, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + assert ( + get_next_market_open("metal", fx_metal_open_weekday_datetime) + == datetime.datetime(2023, 6, 25, 17, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # fx & metal out of market hours + assert ( + get_next_market_open("fx", fx_metal_close_weekend_datetime) + == datetime.datetime(2023, 6, 18, 17, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + assert ( + get_next_market_open("metal", fx_metal_close_weekend_datetime) + == datetime.datetime(2023, 6, 18, 17, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # fx & metal holiday + assert ( + get_next_market_open("fx", fx_metal_holiday_datetime) + == datetime.datetime(2023, 1, 2, 17, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + assert ( + get_next_market_open("metal", fx_metal_holiday_datetime) + == datetime.datetime(2023, 1, 2, 17, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # crypto + assert get_next_market_open("crypto", crypto_open_weekday_datetime) == None + assert get_next_market_open("crypto", crypto_open_weekend_datetime) == None + + +def test_get_next_market_close( + equity_open_weekday_datetime, + equity_close_weekday_datetime, + equity_close_weekend_datetime, + equity_holiday_datetime, + equity_early_holiday_open_datetime, + equity_early_holiday_close_datetime, + fx_metal_open_weekday_datetime, + fx_metal_close_weekend_datetime, + fx_metal_holiday_datetime, + crypto_open_weekday_datetime, + crypto_open_weekend_datetime, +): + # equity within market hours + assert ( + get_next_market_close("equity", equity_open_weekday_datetime) + == datetime.datetime(2023, 6, 21, 16, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # equity out of market hours + assert ( + get_next_market_close("equity", equity_close_weekday_datetime) + == datetime.datetime(2023, 6, 22, 16, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # equity weekend + assert ( + get_next_market_close("equity", equity_close_weekend_datetime) + == datetime.datetime(2023, 6, 12, 16, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # equity holiday + assert ( + get_next_market_close("equity", equity_holiday_datetime) + == datetime.datetime(2023, 6, 20, 16, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # equity early close holiday + assert ( + get_next_market_close("equity", equity_early_holiday_open_datetime) + == datetime.datetime(2023, 11, 24, 13, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + assert ( + get_next_market_close("equity", equity_early_holiday_close_datetime) + == datetime.datetime(2023, 11, 27, 16, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # fx & metal within market hours + assert ( + get_next_market_close("fx", fx_metal_open_weekday_datetime) + == datetime.datetime(2023, 6, 23, 17, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + assert ( + get_next_market_close("metal", fx_metal_open_weekday_datetime) + == datetime.datetime(2023, 6, 23, 17, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # fx & metal out of market hours + assert ( + get_next_market_close("fx", fx_metal_close_weekend_datetime) + == datetime.datetime(2023, 6, 23, 17, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + assert ( + get_next_market_close("metal", fx_metal_close_weekend_datetime) + == datetime.datetime(2023, 6, 23, 17, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # fx & metal holiday + assert ( + get_next_market_close("fx", fx_metal_holiday_datetime) + == datetime.datetime(2023, 1, 6, 17, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + assert ( + get_next_market_close("metal", fx_metal_holiday_datetime) + == datetime.datetime(2023, 1, 6, 17, 0, 0, tzinfo=NY_TZ) + .astimezone(UTC_TZ) + .strftime("%Y-%m-%dT%H:%M:%S") + + "Z" + ) + + # crypto + assert get_next_market_close("crypto", crypto_open_weekday_datetime) == None + assert get_next_market_close("crypto", crypto_open_weekend_datetime) == None From dc5870994e77c49f0be2ae69eb1d0d2b456c8fd1 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Thu, 22 Jun 2023 17:34:12 +0800 Subject: [PATCH 5/6] update python dep to 3.9 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c44fe2b..79b29c1 100644 --- a/setup.py +++ b/setup.py @@ -28,5 +28,5 @@ 'testing': requirements + ['mock', 'pytest', 'pytest-cov', 'pytest-socket', 'pytest-mock', 'pytest-asyncio'], }, - python_requires='>=3.7.0', + python_requires='>=3.9.0', ) From ec2c92db8a9157088ad9dd6233601e9040c2a329 Mon Sep 17 00:00:00 2001 From: Daniel Chew Date: Thu, 22 Jun 2023 17:36:25 +0800 Subject: [PATCH 6/6] update pytest to 3.9 --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 756d032..2f89004 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9"] + python-version: ["3.9"] steps: - uses: actions/checkout@v2