From a62a2d191431baa3ae30e5094bf5d4a1a0233d8c Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Fri, 20 Dec 2024 16:19:33 +0100 Subject: [PATCH 1/6] feat!: add market schedule (#57) This change removes the hard-coded calendar and adds MarketSchedule class to offer the same features using `schedule` metadata in the products. feat!: add market schedule This change removes the hard-coded calendar and adds MarketSchedule class to offer the same features using `schedule` metadata in the products. --- pythclient/calendar.py | 440 -------- pythclient/calendar_full_intervals.py | 1481 ------------------------- pythclient/market_schedule.py | 209 ++++ pythclient/pythaccounts.py | 9 + setup.py | 2 +- tests/test_calendar.py | 489 -------- tests/test_market_schedule.py | 152 +++ 7 files changed, 371 insertions(+), 2411 deletions(-) delete mode 100644 pythclient/calendar.py delete mode 100644 pythclient/calendar_full_intervals.py create mode 100644 pythclient/market_schedule.py delete mode 100644 tests/test_calendar.py create mode 100644 tests/test_market_schedule.py diff --git a/pythclient/calendar.py b/pythclient/calendar.py deleted file mode 100644 index 3d07e6c..0000000 --- a/pythclient/calendar.py +++ /dev/null @@ -1,440 +0,0 @@ -import datetime -from zoneinfo import ZoneInfo - -NY_TZ = ZoneInfo("America/New_York") -UTC_TZ = ZoneInfo("UTC") - -EQUITY_OPEN = datetime.time(9, 30, 0, tzinfo=NY_TZ) -EQUITY_CLOSE = datetime.time(16, 0, 0, tzinfo=NY_TZ) - -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 -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(), - datetime.datetime(2023, 4, 7, tzinfo=NY_TZ).date(), - datetime.datetime(2023, 5, 29, tzinfo=NY_TZ).date(), - datetime.datetime(2023, 6, 19, tzinfo=NY_TZ).date(), - datetime.datetime(2023, 7, 4, tzinfo=NY_TZ).date(), - datetime.datetime(2022, 9, 4, tzinfo=NY_TZ).date(), - datetime.datetime(2023, 11, 23, tzinfo=NY_TZ).date(), - datetime.datetime(2023, 12, 25, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 1, 1, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 1, 15, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 2, 19, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 3, 29, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 5, 27, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 6, 19, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 7, 4, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 9, 2, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 11, 28, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 12, 25, tzinfo=NY_TZ).date(), -] -NYSE_EARLY_HOLIDAYS = [ - datetime.datetime(2023, 7, 3, tzinfo=NY_TZ).date(), - datetime.datetime(2023, 11, 24, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 7, 3, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 11, 29, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 12, 24, tzinfo=NY_TZ).date(), -] - -FX_OPEN_CLOSE_TIME = datetime.time(17, 0, 0, tzinfo=NY_TZ) - -# FX_METAL_HOLIDAYS will need to be updated each year -# From https://www.cboe.com/about/hours/fx/ -FX_HOLIDAYS = [ - datetime.datetime(2023, 1, 1, tzinfo=NY_TZ).date(), - datetime.datetime(2023, 12, 25, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 1, 1, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 12, 25, tzinfo=NY_TZ).date(), -] - -METAL_OPEN_CLOSE_TIME = datetime.time(17, 0, 0, tzinfo=NY_TZ) - - -# References: -# https://www.forex.com/en-ca/help-and-support/market-trading-hours/ -METAL_EARLY_CLOSE = datetime.time(14, 30, 0, tzinfo=NY_TZ) - -# References: -# https://www.ig.com/uk/help-and-support/spread-betting-and-cfds/market-details/martin-luther-king-jr-trading-hours -# https://www.etoro.com/trading/market-hours-and-events/ -METAL_EARLY_CLOSE_OPEN = datetime.time(18, 0, 0, tzinfo=NY_TZ) - -# FX_METAL_HOLIDAYS will need to be updated each year -# From https://www.cboe.com/about/hours/fx/ -METAL_HOLIDAYS = [ - datetime.datetime(2023, 1, 1, tzinfo=NY_TZ).date(), - datetime.datetime(2023, 12, 25, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 1, 1, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 12, 25, tzinfo=NY_TZ).date(), -] -METAL_EARLY_HOLIDAYS = [ - datetime.datetime(2024, 1, 15, tzinfo=NY_TZ).date(), - datetime.datetime(2024, 2, 19, 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 - dt = dt.astimezone(NY_TZ) - day, date, time = dt.weekday(), dt.date(), dt.time() - - if asset_type == "equity": - if date in NYSE_HOLIDAYS or date in NYSE_EARLY_HOLIDAYS: - if ( - date in NYSE_EARLY_HOLIDAYS - and time >= EQUITY_OPEN - and time < NYSE_EARLY_CLOSE - ): - return True - return False - if day < 5 and time >= EQUITY_OPEN and time < EQUITY_CLOSE: - return True - return False - - if asset_type == "fx": - if date in FX_HOLIDAYS and time < FX_OPEN_CLOSE_TIME: - return False - # If the next day is a holiday, the market is closed at 5pm ET - if ( - date + datetime.timedelta(days=1) in FX_HOLIDAYS - ) and time >= FX_OPEN_CLOSE_TIME: - return False - # On Friday the market is closed after 5pm - if day == 4 and time >= FX_OPEN_CLOSE_TIME: - return False - # On Saturday the market is closed all the time - if day == 5: - return False - # On Sunday the market is closed before 5pm - if day == 6 and time < FX_OPEN_CLOSE_TIME: - return False - return True - - if asset_type == "metal": - if date in METAL_HOLIDAYS and time < METAL_OPEN_CLOSE_TIME: - return False - # If the next day is a holiday, the market is closed at 5pm ET - if ( - date + datetime.timedelta(days=1) in METAL_HOLIDAYS - ) and time >= METAL_OPEN_CLOSE_TIME: - return False - if ( - date in METAL_EARLY_HOLIDAYS - and time >= METAL_EARLY_CLOSE - and time < METAL_EARLY_CLOSE_OPEN - ): - return False - # On Friday the market is closed after 5pm - if day == 4 and time >= METAL_OPEN_CLOSE_TIME: - return False - # On Saturday the market is closed all the time - if day == 5: - return False - # On Sunday the market is closed before 5pm - if day == 6 and time < METAL_OPEN_CLOSE_TIME: - return False - 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 - - -def get_next_market_open(asset_type: str, dt: datetime.datetime) -> int: - # make sure time is in NY timezone - dt = dt.astimezone(NY_TZ) - time = dt.time() - - if asset_type == "equity": - if time < EQUITY_OPEN: - next_market_open = dt.replace( - hour=EQUITY_OPEN.hour, - minute=EQUITY_OPEN.minute, - second=0, - microsecond=0, - ) - else: - next_market_open = dt.replace( - hour=EQUITY_OPEN.hour, - minute=EQUITY_OPEN.minute, - second=0, - microsecond=0, - ) - next_market_open += datetime.timedelta(days=1) - elif asset_type == "fx": - if (dt.weekday() == 6 and time < FX_OPEN_CLOSE_TIME) or ( - dt.date() in FX_HOLIDAYS and time < FX_OPEN_CLOSE_TIME - ): - next_market_open = dt.replace( - hour=FX_OPEN_CLOSE_TIME.hour, - minute=FX_OPEN_CLOSE_TIME.minute, - second=0, - microsecond=0, - ) - else: - next_market_open = dt.replace( - hour=FX_OPEN_CLOSE_TIME.hour, - minute=FX_OPEN_CLOSE_TIME.minute, - second=0, - microsecond=0, - ) - while is_market_open(asset_type, next_market_open): - next_market_open += datetime.timedelta(days=1) - elif asset_type == "metal": - if dt.date() in METAL_EARLY_HOLIDAYS and time < METAL_EARLY_CLOSE_OPEN: - next_market_open = dt.replace( - hour=METAL_EARLY_CLOSE_OPEN.hour, - minute=METAL_EARLY_CLOSE_OPEN.minute, - second=0, - microsecond=0, - ) - elif dt.date() in METAL_EARLY_HOLIDAYS and time >= METAL_EARLY_CLOSE_OPEN: - next_market_open = dt.replace( - hour=METAL_OPEN_CLOSE_TIME.hour, - minute=METAL_OPEN_CLOSE_TIME.minute, - 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: - if (dt.weekday() == 6 and time < METAL_OPEN_CLOSE_TIME) or ( - dt.date() in METAL_HOLIDAYS and time < METAL_OPEN_CLOSE_TIME - ): - next_market_open = dt.replace( - hour=METAL_OPEN_CLOSE_TIME.hour, - minute=METAL_OPEN_CLOSE_TIME.minute, - second=0, - microsecond=0, - ) - else: - next_market_open = dt.replace( - hour=METAL_OPEN_CLOSE_TIME.hour, - minute=METAL_OPEN_CLOSE_TIME.minute, - second=0, - microsecond=0, - ) - 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 - - while not is_market_open(asset_type, next_market_open): - next_market_open += datetime.timedelta(days=1) - - return int(next_market_open.timestamp()) - - -def get_next_market_close(asset_type: str, dt: datetime.datetime) -> int: - # make sure time is in NY timezone - dt = dt.astimezone(NY_TZ) - time = dt.time() - - if asset_type == "equity": - 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=EQUITY_CLOSE.hour, - minute=EQUITY_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=EQUITY_CLOSE.hour, - minute=EQUITY_CLOSE.minute, - second=0, - microsecond=0, - ) - ) - else: - next_market_close = dt.replace( - hour=EQUITY_CLOSE.hour, - minute=EQUITY_CLOSE.minute, - second=0, - microsecond=0, - ) - if time >= EQUITY_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) - - elif asset_type == "fx": - next_market_close = dt.replace( - hour=FX_OPEN_CLOSE_TIME.hour, - minute=FX_OPEN_CLOSE_TIME.minute, - second=0, - microsecond=0, - ) - if dt.weekday() != 4: - 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) - elif asset_type == "metal": - if dt.date() in METAL_EARLY_HOLIDAYS and time < METAL_EARLY_CLOSE: - next_market_close = dt.replace( - hour=METAL_EARLY_CLOSE.hour, - minute=METAL_EARLY_CLOSE.minute, - second=0, - microsecond=0, - ) - elif dt.date() in METAL_EARLY_HOLIDAYS and time >= METAL_EARLY_CLOSE: - next_market_close = dt.replace( - hour=METAL_OPEN_CLOSE_TIME.hour, - minute=METAL_OPEN_CLOSE_TIME.minute, - second=0, - microsecond=0, - ) - next_market_close += datetime.timedelta(days=1) - while is_market_open(asset_type, next_market_close): - next_market_close += datetime.timedelta(days=1) - else: - next_market_close = dt.replace( - hour=METAL_OPEN_CLOSE_TIME.hour, - minute=METAL_OPEN_CLOSE_TIME.minute, - second=0, - microsecond=0, - ) - if dt.weekday() != 4: - 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) - 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 - - return int(next_market_close.timestamp()) diff --git a/pythclient/calendar_full_intervals.py b/pythclient/calendar_full_intervals.py deleted file mode 100644 index 0ed8aba..0000000 --- a/pythclient/calendar_full_intervals.py +++ /dev/null @@ -1,1481 +0,0 @@ -import datetime - -EQUITY_2024_INTERVALS = [ - (datetime.date(2024, 1, 1), None), - (datetime.date(2024, 1, 2), "0930-1600"), - (datetime.date(2024, 1, 3), "0930-1600"), - (datetime.date(2024, 1, 4), "0930-1600"), - (datetime.date(2024, 1, 5), "0930-1600"), - (datetime.date(2024, 1, 6), None), - (datetime.date(2024, 1, 7), None), - (datetime.date(2024, 1, 8), "0930-1600"), - (datetime.date(2024, 1, 9), "0930-1600"), - (datetime.date(2024, 1, 10), "0930-1600"), - (datetime.date(2024, 1, 11), "0930-1600"), - (datetime.date(2024, 1, 12), "0930-1600"), - (datetime.date(2024, 1, 13), None), - (datetime.date(2024, 1, 14), None), - (datetime.date(2024, 1, 15), None), - (datetime.date(2024, 1, 16), "0930-1600"), - (datetime.date(2024, 1, 17), "0930-1600"), - (datetime.date(2024, 1, 18), "0930-1600"), - (datetime.date(2024, 1, 19), "0930-1600"), - (datetime.date(2024, 1, 20), None), - (datetime.date(2024, 1, 21), None), - (datetime.date(2024, 1, 22), "0930-1600"), - (datetime.date(2024, 1, 23), "0930-1600"), - (datetime.date(2024, 1, 24), "0930-1600"), - (datetime.date(2024, 1, 25), "0930-1600"), - (datetime.date(2024, 1, 26), "0930-1600"), - (datetime.date(2024, 1, 27), None), - (datetime.date(2024, 1, 28), None), - (datetime.date(2024, 1, 29), "0930-1600"), - (datetime.date(2024, 1, 30), "0930-1600"), - (datetime.date(2024, 1, 31), "0930-1600"), - (datetime.date(2024, 2, 1), "0930-1600"), - (datetime.date(2024, 2, 2), "0930-1600"), - (datetime.date(2024, 2, 3), None), - (datetime.date(2024, 2, 4), None), - (datetime.date(2024, 2, 5), "0930-1600"), - (datetime.date(2024, 2, 6), "0930-1600"), - (datetime.date(2024, 2, 7), "0930-1600"), - (datetime.date(2024, 2, 8), "0930-1600"), - (datetime.date(2024, 2, 9), "0930-1600"), - (datetime.date(2024, 2, 10), None), - (datetime.date(2024, 2, 11), None), - (datetime.date(2024, 2, 12), "0930-1600"), - (datetime.date(2024, 2, 13), "0930-1600"), - (datetime.date(2024, 2, 14), "0930-1600"), - (datetime.date(2024, 2, 15), "0930-1600"), - (datetime.date(2024, 2, 16), "0930-1600"), - (datetime.date(2024, 2, 17), None), - (datetime.date(2024, 2, 18), None), - (datetime.date(2024, 2, 19), None), - (datetime.date(2024, 2, 20), "0930-1600"), - (datetime.date(2024, 2, 21), "0930-1600"), - (datetime.date(2024, 2, 22), "0930-1600"), - (datetime.date(2024, 2, 23), "0930-1600"), - (datetime.date(2024, 2, 24), None), - (datetime.date(2024, 2, 25), None), - (datetime.date(2024, 2, 26), "0930-1600"), - (datetime.date(2024, 2, 27), "0930-1600"), - (datetime.date(2024, 2, 28), "0930-1600"), - (datetime.date(2024, 2, 29), "0930-1600"), - (datetime.date(2024, 3, 1), "0930-1600"), - (datetime.date(2024, 3, 2), None), - (datetime.date(2024, 3, 3), None), - (datetime.date(2024, 3, 4), "0930-1600"), - (datetime.date(2024, 3, 5), "0930-1600"), - (datetime.date(2024, 3, 6), "0930-1600"), - (datetime.date(2024, 3, 7), "0930-1600"), - (datetime.date(2024, 3, 8), "0930-1600"), - (datetime.date(2024, 3, 9), None), - (datetime.date(2024, 3, 10), None), - (datetime.date(2024, 3, 11), "0930-1600"), - (datetime.date(2024, 3, 12), "0930-1600"), - (datetime.date(2024, 3, 13), "0930-1600"), - (datetime.date(2024, 3, 14), "0930-1600"), - (datetime.date(2024, 3, 15), "0930-1600"), - (datetime.date(2024, 3, 16), None), - (datetime.date(2024, 3, 17), None), - (datetime.date(2024, 3, 18), "0930-1600"), - (datetime.date(2024, 3, 19), "0930-1600"), - (datetime.date(2024, 3, 20), "0930-1600"), - (datetime.date(2024, 3, 21), "0930-1600"), - (datetime.date(2024, 3, 22), "0930-1600"), - (datetime.date(2024, 3, 23), None), - (datetime.date(2024, 3, 24), None), - (datetime.date(2024, 3, 25), "0930-1600"), - (datetime.date(2024, 3, 26), "0930-1600"), - (datetime.date(2024, 3, 27), "0930-1600"), - (datetime.date(2024, 3, 28), "0930-1600"), - (datetime.date(2024, 3, 29), None), - (datetime.date(2024, 3, 30), None), - (datetime.date(2024, 3, 31), None), - (datetime.date(2024, 4, 1), "0930-1600"), - (datetime.date(2024, 4, 2), "0930-1600"), - (datetime.date(2024, 4, 3), "0930-1600"), - (datetime.date(2024, 4, 4), "0930-1600"), - (datetime.date(2024, 4, 5), "0930-1600"), - (datetime.date(2024, 4, 6), None), - (datetime.date(2024, 4, 7), None), - (datetime.date(2024, 4, 8), "0930-1600"), - (datetime.date(2024, 4, 9), "0930-1600"), - (datetime.date(2024, 4, 10), "0930-1600"), - (datetime.date(2024, 4, 11), "0930-1600"), - (datetime.date(2024, 4, 12), "0930-1600"), - (datetime.date(2024, 4, 13), None), - (datetime.date(2024, 4, 14), None), - (datetime.date(2024, 4, 15), "0930-1600"), - (datetime.date(2024, 4, 16), "0930-1600"), - (datetime.date(2024, 4, 17), "0930-1600"), - (datetime.date(2024, 4, 18), "0930-1600"), - (datetime.date(2024, 4, 19), "0930-1600"), - (datetime.date(2024, 4, 20), None), - (datetime.date(2024, 4, 21), None), - (datetime.date(2024, 4, 22), "0930-1600"), - (datetime.date(2024, 4, 23), "0930-1600"), - (datetime.date(2024, 4, 24), "0930-1600"), - (datetime.date(2024, 4, 25), "0930-1600"), - (datetime.date(2024, 4, 26), "0930-1600"), - (datetime.date(2024, 4, 27), None), - (datetime.date(2024, 4, 28), None), - (datetime.date(2024, 4, 29), "0930-1600"), - (datetime.date(2024, 4, 30), "0930-1600"), - (datetime.date(2024, 5, 1), "0930-1600"), - (datetime.date(2024, 5, 2), "0930-1600"), - (datetime.date(2024, 5, 3), "0930-1600"), - (datetime.date(2024, 5, 4), None), - (datetime.date(2024, 5, 5), None), - (datetime.date(2024, 5, 6), "0930-1600"), - (datetime.date(2024, 5, 7), "0930-1600"), - (datetime.date(2024, 5, 8), "0930-1600"), - (datetime.date(2024, 5, 9), "0930-1600"), - (datetime.date(2024, 5, 10), "0930-1600"), - (datetime.date(2024, 5, 11), None), - (datetime.date(2024, 5, 12), None), - (datetime.date(2024, 5, 13), "0930-1600"), - (datetime.date(2024, 5, 14), "0930-1600"), - (datetime.date(2024, 5, 15), "0930-1600"), - (datetime.date(2024, 5, 16), "0930-1600"), - (datetime.date(2024, 5, 17), "0930-1600"), - (datetime.date(2024, 5, 18), None), - (datetime.date(2024, 5, 19), None), - (datetime.date(2024, 5, 20), "0930-1600"), - (datetime.date(2024, 5, 21), "0930-1600"), - (datetime.date(2024, 5, 22), "0930-1600"), - (datetime.date(2024, 5, 23), "0930-1600"), - (datetime.date(2024, 5, 24), "0930-1600"), - (datetime.date(2024, 5, 25), None), - (datetime.date(2024, 5, 26), None), - (datetime.date(2024, 5, 27), None), - (datetime.date(2024, 5, 28), "0930-1600"), - (datetime.date(2024, 5, 29), "0930-1600"), - (datetime.date(2024, 5, 30), "0930-1600"), - (datetime.date(2024, 5, 31), "0930-1600"), - (datetime.date(2024, 6, 1), None), - (datetime.date(2024, 6, 2), None), - (datetime.date(2024, 6, 3), "0930-1600"), - (datetime.date(2024, 6, 4), "0930-1600"), - (datetime.date(2024, 6, 5), "0930-1600"), - (datetime.date(2024, 6, 6), "0930-1600"), - (datetime.date(2024, 6, 7), "0930-1600"), - (datetime.date(2024, 6, 8), None), - (datetime.date(2024, 6, 9), None), - (datetime.date(2024, 6, 10), "0930-1600"), - (datetime.date(2024, 6, 11), "0930-1600"), - (datetime.date(2024, 6, 12), "0930-1600"), - (datetime.date(2024, 6, 13), "0930-1600"), - (datetime.date(2024, 6, 14), "0930-1600"), - (datetime.date(2024, 6, 15), None), - (datetime.date(2024, 6, 16), None), - (datetime.date(2024, 6, 17), "0930-1600"), - (datetime.date(2024, 6, 18), "0930-1600"), - (datetime.date(2024, 6, 19), None), - (datetime.date(2024, 6, 20), "0930-1600"), - (datetime.date(2024, 6, 21), "0930-1600"), - (datetime.date(2024, 6, 22), None), - (datetime.date(2024, 6, 23), None), - (datetime.date(2024, 6, 24), "0930-1600"), - (datetime.date(2024, 6, 25), "0930-1600"), - (datetime.date(2024, 6, 26), "0930-1600"), - (datetime.date(2024, 6, 27), "0930-1600"), - (datetime.date(2024, 6, 28), "0930-1600"), - (datetime.date(2024, 6, 29), None), - (datetime.date(2024, 6, 30), None), - (datetime.date(2024, 7, 1), "0930-1600"), - (datetime.date(2024, 7, 2), "0930-1600"), - (datetime.date(2024, 7, 3), "0930-1300"), - (datetime.date(2024, 7, 4), None), - (datetime.date(2024, 7, 5), "0930-1600"), - (datetime.date(2024, 7, 6), None), - (datetime.date(2024, 7, 7), None), - (datetime.date(2024, 7, 8), "0930-1600"), - (datetime.date(2024, 7, 9), "0930-1600"), - (datetime.date(2024, 7, 10), "0930-1600"), - (datetime.date(2024, 7, 11), "0930-1600"), - (datetime.date(2024, 7, 12), "0930-1600"), - (datetime.date(2024, 7, 13), None), - (datetime.date(2024, 7, 14), None), - (datetime.date(2024, 7, 15), "0930-1600"), - (datetime.date(2024, 7, 16), "0930-1600"), - (datetime.date(2024, 7, 17), "0930-1600"), - (datetime.date(2024, 7, 18), "0930-1600"), - (datetime.date(2024, 7, 19), "0930-1600"), - (datetime.date(2024, 7, 20), None), - (datetime.date(2024, 7, 21), None), - (datetime.date(2024, 7, 22), "0930-1600"), - (datetime.date(2024, 7, 23), "0930-1600"), - (datetime.date(2024, 7, 24), "0930-1600"), - (datetime.date(2024, 7, 25), "0930-1600"), - (datetime.date(2024, 7, 26), "0930-1600"), - (datetime.date(2024, 7, 27), None), - (datetime.date(2024, 7, 28), None), - (datetime.date(2024, 7, 29), "0930-1600"), - (datetime.date(2024, 7, 30), "0930-1600"), - (datetime.date(2024, 7, 31), "0930-1600"), - (datetime.date(2024, 8, 1), "0930-1600"), - (datetime.date(2024, 8, 2), "0930-1600"), - (datetime.date(2024, 8, 3), None), - (datetime.date(2024, 8, 4), None), - (datetime.date(2024, 8, 5), "0930-1600"), - (datetime.date(2024, 8, 6), "0930-1600"), - (datetime.date(2024, 8, 7), "0930-1600"), - (datetime.date(2024, 8, 8), "0930-1600"), - (datetime.date(2024, 8, 9), "0930-1600"), - (datetime.date(2024, 8, 10), None), - (datetime.date(2024, 8, 11), None), - (datetime.date(2024, 8, 12), "0930-1600"), - (datetime.date(2024, 8, 13), "0930-1600"), - (datetime.date(2024, 8, 14), "0930-1600"), - (datetime.date(2024, 8, 15), "0930-1600"), - (datetime.date(2024, 8, 16), "0930-1600"), - (datetime.date(2024, 8, 17), None), - (datetime.date(2024, 8, 18), None), - (datetime.date(2024, 8, 19), "0930-1600"), - (datetime.date(2024, 8, 20), "0930-1600"), - (datetime.date(2024, 8, 21), "0930-1600"), - (datetime.date(2024, 8, 22), "0930-1600"), - (datetime.date(2024, 8, 23), "0930-1600"), - (datetime.date(2024, 8, 24), None), - (datetime.date(2024, 8, 25), None), - (datetime.date(2024, 8, 26), "0930-1600"), - (datetime.date(2024, 8, 27), "0930-1600"), - (datetime.date(2024, 8, 28), "0930-1600"), - (datetime.date(2024, 8, 29), "0930-1600"), - (datetime.date(2024, 8, 30), "0930-1600"), - (datetime.date(2024, 8, 31), None), - (datetime.date(2024, 9, 1), None), - (datetime.date(2024, 9, 2), None), - (datetime.date(2024, 9, 3), "0930-1600"), - (datetime.date(2024, 9, 4), "0930-1600"), - (datetime.date(2024, 9, 5), "0930-1600"), - (datetime.date(2024, 9, 6), "0930-1600"), - (datetime.date(2024, 9, 7), None), - (datetime.date(2024, 9, 8), None), - (datetime.date(2024, 9, 9), "0930-1600"), - (datetime.date(2024, 9, 10), "0930-1600"), - (datetime.date(2024, 9, 11), "0930-1600"), - (datetime.date(2024, 9, 12), "0930-1600"), - (datetime.date(2024, 9, 13), "0930-1600"), - (datetime.date(2024, 9, 14), None), - (datetime.date(2024, 9, 15), None), - (datetime.date(2024, 9, 16), "0930-1600"), - (datetime.date(2024, 9, 17), "0930-1600"), - (datetime.date(2024, 9, 18), "0930-1600"), - (datetime.date(2024, 9, 19), "0930-1600"), - (datetime.date(2024, 9, 20), "0930-1600"), - (datetime.date(2024, 9, 21), None), - (datetime.date(2024, 9, 22), None), - (datetime.date(2024, 9, 23), "0930-1600"), - (datetime.date(2024, 9, 24), "0930-1600"), - (datetime.date(2024, 9, 25), "0930-1600"), - (datetime.date(2024, 9, 26), "0930-1600"), - (datetime.date(2024, 9, 27), "0930-1600"), - (datetime.date(2024, 9, 28), None), - (datetime.date(2024, 9, 29), None), - (datetime.date(2024, 9, 30), "0930-1600"), - (datetime.date(2024, 10, 1), "0930-1600"), - (datetime.date(2024, 10, 2), "0930-1600"), - (datetime.date(2024, 10, 3), "0930-1600"), - (datetime.date(2024, 10, 4), "0930-1600"), - (datetime.date(2024, 10, 5), None), - (datetime.date(2024, 10, 6), None), - (datetime.date(2024, 10, 7), "0930-1600"), - (datetime.date(2024, 10, 8), "0930-1600"), - (datetime.date(2024, 10, 9), "0930-1600"), - (datetime.date(2024, 10, 10), "0930-1600"), - (datetime.date(2024, 10, 11), "0930-1600"), - (datetime.date(2024, 10, 12), None), - (datetime.date(2024, 10, 13), None), - (datetime.date(2024, 10, 14), "0930-1600"), - (datetime.date(2024, 10, 15), "0930-1600"), - (datetime.date(2024, 10, 16), "0930-1600"), - (datetime.date(2024, 10, 17), "0930-1600"), - (datetime.date(2024, 10, 18), "0930-1600"), - (datetime.date(2024, 10, 19), None), - (datetime.date(2024, 10, 20), None), - (datetime.date(2024, 10, 21), "0930-1600"), - (datetime.date(2024, 10, 22), "0930-1600"), - (datetime.date(2024, 10, 23), "0930-1600"), - (datetime.date(2024, 10, 24), "0930-1600"), - (datetime.date(2024, 10, 25), "0930-1600"), - (datetime.date(2024, 10, 26), None), - (datetime.date(2024, 10, 27), None), - (datetime.date(2024, 10, 28), "0930-1600"), - (datetime.date(2024, 10, 29), "0930-1600"), - (datetime.date(2024, 10, 30), "0930-1600"), - (datetime.date(2024, 10, 31), "0930-1600"), - (datetime.date(2024, 11, 1), "0930-1600"), - (datetime.date(2024, 11, 2), None), - (datetime.date(2024, 11, 3), None), - (datetime.date(2024, 11, 4), "0930-1600"), - (datetime.date(2024, 11, 5), "0930-1600"), - (datetime.date(2024, 11, 6), "0930-1600"), - (datetime.date(2024, 11, 7), "0930-1600"), - (datetime.date(2024, 11, 8), "0930-1600"), - (datetime.date(2024, 11, 9), None), - (datetime.date(2024, 11, 10), None), - (datetime.date(2024, 11, 11), "0930-1600"), - (datetime.date(2024, 11, 12), "0930-1600"), - (datetime.date(2024, 11, 13), "0930-1600"), - (datetime.date(2024, 11, 14), "0930-1600"), - (datetime.date(2024, 11, 15), "0930-1600"), - (datetime.date(2024, 11, 16), None), - (datetime.date(2024, 11, 17), None), - (datetime.date(2024, 11, 18), "0930-1600"), - (datetime.date(2024, 11, 19), "0930-1600"), - (datetime.date(2024, 11, 20), "0930-1600"), - (datetime.date(2024, 11, 21), "0930-1600"), - (datetime.date(2024, 11, 22), "0930-1600"), - (datetime.date(2024, 11, 23), None), - (datetime.date(2024, 11, 24), None), - (datetime.date(2024, 11, 25), "0930-1600"), - (datetime.date(2024, 11, 26), "0930-1600"), - (datetime.date(2024, 11, 27), "0930-1600"), - (datetime.date(2024, 11, 28), None), - (datetime.date(2024, 11, 29), "0930-1300"), - (datetime.date(2024, 11, 30), None), - (datetime.date(2024, 12, 1), None), - (datetime.date(2024, 12, 2), "0930-1600"), - (datetime.date(2024, 12, 3), "0930-1600"), - (datetime.date(2024, 12, 4), "0930-1600"), - (datetime.date(2024, 12, 5), "0930-1600"), - (datetime.date(2024, 12, 6), "0930-1600"), - (datetime.date(2024, 12, 7), None), - (datetime.date(2024, 12, 8), None), - (datetime.date(2024, 12, 9), "0930-1600"), - (datetime.date(2024, 12, 10), "0930-1600"), - (datetime.date(2024, 12, 11), "0930-1600"), - (datetime.date(2024, 12, 12), "0930-1600"), - (datetime.date(2024, 12, 13), "0930-1600"), - (datetime.date(2024, 12, 14), None), - (datetime.date(2024, 12, 15), None), - (datetime.date(2024, 12, 16), "0930-1600"), - (datetime.date(2024, 12, 17), "0930-1600"), - (datetime.date(2024, 12, 18), "0930-1600"), - (datetime.date(2024, 12, 19), "0930-1600"), - (datetime.date(2024, 12, 20), "0930-1600"), - (datetime.date(2024, 12, 21), None), - (datetime.date(2024, 12, 22), None), - (datetime.date(2024, 12, 23), "0930-1600"), - (datetime.date(2024, 12, 24), "0930-1300"), - (datetime.date(2024, 12, 25), None), - (datetime.date(2024, 12, 26), "0930-1600"), - (datetime.date(2024, 12, 27), "0930-1600"), - (datetime.date(2024, 12, 28), None), - (datetime.date(2024, 12, 29), None), - (datetime.date(2024, 12, 30), "0930-1600"), - (datetime.date(2024, 12, 31), "0930-1600"), -] - -FX_2024_INTERVALS = [ - (datetime.date(2023, 12, 31), None), - (datetime.date(2024, 1, 1), "1700-0000"), - (datetime.date(2024, 1, 2), "0000-0000"), - (datetime.date(2024, 1, 3), "0000-0000"), - (datetime.date(2024, 1, 4), "0000-0000"), - (datetime.date(2024, 1, 5), "0000-1700"), - (datetime.date(2024, 1, 6), None), - (datetime.date(2024, 1, 7), "1700-0000"), - (datetime.date(2024, 1, 8), "0000-0000"), - (datetime.date(2024, 1, 9), "0000-0000"), - (datetime.date(2024, 1, 10), "0000-0000"), - (datetime.date(2024, 1, 11), "0000-0000"), - (datetime.date(2024, 1, 12), "0000-1700"), - (datetime.date(2024, 1, 13), None), - (datetime.date(2024, 1, 14), "1700-0000"), - (datetime.date(2024, 1, 15), "0000-0000"), - (datetime.date(2024, 1, 16), "0000-0000"), - (datetime.date(2024, 1, 17), "0000-0000"), - (datetime.date(2024, 1, 18), "0000-0000"), - (datetime.date(2024, 1, 19), "0000-1700"), - (datetime.date(2024, 1, 20), None), - (datetime.date(2024, 1, 21), "1700-0000"), - (datetime.date(2024, 1, 22), "0000-0000"), - (datetime.date(2024, 1, 23), "0000-0000"), - (datetime.date(2024, 1, 24), "0000-0000"), - (datetime.date(2024, 1, 25), "0000-0000"), - (datetime.date(2024, 1, 26), "0000-1700"), - (datetime.date(2024, 1, 27), None), - (datetime.date(2024, 1, 28), "1700-0000"), - (datetime.date(2024, 1, 29), "0000-0000"), - (datetime.date(2024, 1, 30), "0000-0000"), - (datetime.date(2024, 1, 31), "0000-0000"), - (datetime.date(2024, 2, 1), "0000-0000"), - (datetime.date(2024, 2, 2), "0000-1700"), - (datetime.date(2024, 2, 3), None), - (datetime.date(2024, 2, 4), "1700-0000"), - (datetime.date(2024, 2, 5), "0000-0000"), - (datetime.date(2024, 2, 6), "0000-0000"), - (datetime.date(2024, 2, 7), "0000-0000"), - (datetime.date(2024, 2, 8), "0000-0000"), - (datetime.date(2024, 2, 9), "0000-1700"), - (datetime.date(2024, 2, 10), None), - (datetime.date(2024, 2, 11), "1700-0000"), - (datetime.date(2024, 2, 12), "0000-0000"), - (datetime.date(2024, 2, 13), "0000-0000"), - (datetime.date(2024, 2, 14), "0000-0000"), - (datetime.date(2024, 2, 15), "0000-0000"), - (datetime.date(2024, 2, 16), "0000-1700"), - (datetime.date(2024, 2, 17), None), - (datetime.date(2024, 2, 18), "1700-0000"), - (datetime.date(2024, 2, 19), "0000-0000"), - (datetime.date(2024, 2, 20), "0000-0000"), - (datetime.date(2024, 2, 21), "0000-0000"), - (datetime.date(2024, 2, 22), "0000-0000"), - (datetime.date(2024, 2, 23), "0000-1700"), - (datetime.date(2024, 2, 24), None), - (datetime.date(2024, 2, 25), "1700-0000"), - (datetime.date(2024, 2, 26), "0000-0000"), - (datetime.date(2024, 2, 27), "0000-0000"), - (datetime.date(2024, 2, 28), "0000-0000"), - (datetime.date(2024, 2, 29), "0000-0000"), - (datetime.date(2024, 3, 1), "0000-1700"), - (datetime.date(2024, 3, 2), None), - (datetime.date(2024, 3, 3), "1700-0000"), - (datetime.date(2024, 3, 4), "0000-0000"), - (datetime.date(2024, 3, 5), "0000-0000"), - (datetime.date(2024, 3, 6), "0000-0000"), - (datetime.date(2024, 3, 7), "0000-0000"), - (datetime.date(2024, 3, 8), "0000-1700"), - (datetime.date(2024, 3, 9), None), - (datetime.date(2024, 3, 10), "1700-0000"), - (datetime.date(2024, 3, 11), "0000-0000"), - (datetime.date(2024, 3, 12), "0000-0000"), - (datetime.date(2024, 3, 13), "0000-0000"), - (datetime.date(2024, 3, 14), "0000-0000"), - (datetime.date(2024, 3, 15), "0000-1700"), - (datetime.date(2024, 3, 16), None), - (datetime.date(2024, 3, 17), "1700-0000"), - (datetime.date(2024, 3, 18), "0000-0000"), - (datetime.date(2024, 3, 19), "0000-0000"), - (datetime.date(2024, 3, 20), "0000-0000"), - (datetime.date(2024, 3, 21), "0000-0000"), - (datetime.date(2024, 3, 22), "0000-1700"), - (datetime.date(2024, 3, 23), None), - (datetime.date(2024, 3, 24), "1700-0000"), - (datetime.date(2024, 3, 25), "0000-0000"), - (datetime.date(2024, 3, 26), "0000-0000"), - (datetime.date(2024, 3, 27), "0000-0000"), - (datetime.date(2024, 3, 28), "0000-0000"), - (datetime.date(2024, 3, 29), "0000-1700"), - (datetime.date(2024, 3, 30), None), - (datetime.date(2024, 3, 31), "1700-0000"), - (datetime.date(2024, 4, 1), "0000-0000"), - (datetime.date(2024, 4, 2), "0000-0000"), - (datetime.date(2024, 4, 3), "0000-0000"), - (datetime.date(2024, 4, 4), "0000-0000"), - (datetime.date(2024, 4, 5), "0000-1700"), - (datetime.date(2024, 4, 6), None), - (datetime.date(2024, 4, 7), "1700-0000"), - (datetime.date(2024, 4, 8), "0000-0000"), - (datetime.date(2024, 4, 9), "0000-0000"), - (datetime.date(2024, 4, 10), "0000-0000"), - (datetime.date(2024, 4, 11), "0000-0000"), - (datetime.date(2024, 4, 12), "0000-1700"), - (datetime.date(2024, 4, 13), None), - (datetime.date(2024, 4, 14), "1700-0000"), - (datetime.date(2024, 4, 15), "0000-0000"), - (datetime.date(2024, 4, 16), "0000-0000"), - (datetime.date(2024, 4, 17), "0000-0000"), - (datetime.date(2024, 4, 18), "0000-0000"), - (datetime.date(2024, 4, 19), "0000-1700"), - (datetime.date(2024, 4, 20), None), - (datetime.date(2024, 4, 21), "1700-0000"), - (datetime.date(2024, 4, 22), "0000-0000"), - (datetime.date(2024, 4, 23), "0000-0000"), - (datetime.date(2024, 4, 24), "0000-0000"), - (datetime.date(2024, 4, 25), "0000-0000"), - (datetime.date(2024, 4, 26), "0000-1700"), - (datetime.date(2024, 4, 27), None), - (datetime.date(2024, 4, 28), "1700-0000"), - (datetime.date(2024, 4, 29), "0000-0000"), - (datetime.date(2024, 4, 30), "0000-0000"), - (datetime.date(2024, 5, 1), "0000-0000"), - (datetime.date(2024, 5, 2), "0000-0000"), - (datetime.date(2024, 5, 3), "0000-1700"), - (datetime.date(2024, 5, 4), None), - (datetime.date(2024, 5, 5), "1700-0000"), - (datetime.date(2024, 5, 6), "0000-0000"), - (datetime.date(2024, 5, 7), "0000-0000"), - (datetime.date(2024, 5, 8), "0000-0000"), - (datetime.date(2024, 5, 9), "0000-0000"), - (datetime.date(2024, 5, 10), "0000-1700"), - (datetime.date(2024, 5, 11), None), - (datetime.date(2024, 5, 12), "1700-0000"), - (datetime.date(2024, 5, 13), "0000-0000"), - (datetime.date(2024, 5, 14), "0000-0000"), - (datetime.date(2024, 5, 15), "0000-0000"), - (datetime.date(2024, 5, 16), "0000-0000"), - (datetime.date(2024, 5, 17), "0000-1700"), - (datetime.date(2024, 5, 18), None), - (datetime.date(2024, 5, 19), "1700-0000"), - (datetime.date(2024, 5, 20), "0000-0000"), - (datetime.date(2024, 5, 21), "0000-0000"), - (datetime.date(2024, 5, 22), "0000-0000"), - (datetime.date(2024, 5, 23), "0000-0000"), - (datetime.date(2024, 5, 24), "0000-1700"), - (datetime.date(2024, 5, 25), None), - (datetime.date(2024, 5, 26), "1700-0000"), - (datetime.date(2024, 5, 27), "0000-0000"), - (datetime.date(2024, 5, 28), "0000-0000"), - (datetime.date(2024, 5, 29), "0000-0000"), - (datetime.date(2024, 5, 30), "0000-0000"), - (datetime.date(2024, 5, 31), "0000-1700"), - (datetime.date(2024, 6, 1), None), - (datetime.date(2024, 6, 2), "1700-0000"), - (datetime.date(2024, 6, 3), "0000-0000"), - (datetime.date(2024, 6, 4), "0000-0000"), - (datetime.date(2024, 6, 5), "0000-0000"), - (datetime.date(2024, 6, 6), "0000-0000"), - (datetime.date(2024, 6, 7), "0000-1700"), - (datetime.date(2024, 6, 8), None), - (datetime.date(2024, 6, 9), "1700-0000"), - (datetime.date(2024, 6, 10), "0000-0000"), - (datetime.date(2024, 6, 11), "0000-0000"), - (datetime.date(2024, 6, 12), "0000-0000"), - (datetime.date(2024, 6, 13), "0000-0000"), - (datetime.date(2024, 6, 14), "0000-1700"), - (datetime.date(2024, 6, 15), None), - (datetime.date(2024, 6, 16), "1700-0000"), - (datetime.date(2024, 6, 17), "0000-0000"), - (datetime.date(2024, 6, 18), "0000-0000"), - (datetime.date(2024, 6, 19), "0000-0000"), - (datetime.date(2024, 6, 20), "0000-0000"), - (datetime.date(2024, 6, 21), "0000-1700"), - (datetime.date(2024, 6, 22), None), - (datetime.date(2024, 6, 23), "1700-0000"), - (datetime.date(2024, 6, 24), "0000-0000"), - (datetime.date(2024, 6, 25), "0000-0000"), - (datetime.date(2024, 6, 26), "0000-0000"), - (datetime.date(2024, 6, 27), "0000-0000"), - (datetime.date(2024, 6, 28), "0000-1700"), - (datetime.date(2024, 6, 29), None), - (datetime.date(2024, 6, 30), "1700-0000"), - (datetime.date(2024, 7, 1), "0000-0000"), - (datetime.date(2024, 7, 2), "0000-0000"), - (datetime.date(2024, 7, 3), "0000-0000"), - (datetime.date(2024, 7, 4), "0000-0000"), - (datetime.date(2024, 7, 5), "0000-1700"), - (datetime.date(2024, 7, 6), None), - (datetime.date(2024, 7, 7), "1700-0000"), - (datetime.date(2024, 7, 8), "0000-0000"), - (datetime.date(2024, 7, 9), "0000-0000"), - (datetime.date(2024, 7, 10), "0000-0000"), - (datetime.date(2024, 7, 11), "0000-0000"), - (datetime.date(2024, 7, 12), "0000-1700"), - (datetime.date(2024, 7, 13), None), - (datetime.date(2024, 7, 14), "1700-0000"), - (datetime.date(2024, 7, 15), "0000-0000"), - (datetime.date(2024, 7, 16), "0000-0000"), - (datetime.date(2024, 7, 17), "0000-0000"), - (datetime.date(2024, 7, 18), "0000-0000"), - (datetime.date(2024, 7, 19), "0000-1700"), - (datetime.date(2024, 7, 20), None), - (datetime.date(2024, 7, 21), "1700-0000"), - (datetime.date(2024, 7, 22), "0000-0000"), - (datetime.date(2024, 7, 23), "0000-0000"), - (datetime.date(2024, 7, 24), "0000-0000"), - (datetime.date(2024, 7, 25), "0000-0000"), - (datetime.date(2024, 7, 26), "0000-1700"), - (datetime.date(2024, 7, 27), None), - (datetime.date(2024, 7, 28), "1700-0000"), - (datetime.date(2024, 7, 29), "0000-0000"), - (datetime.date(2024, 7, 30), "0000-0000"), - (datetime.date(2024, 7, 31), "0000-0000"), - (datetime.date(2024, 8, 1), "0000-0000"), - (datetime.date(2024, 8, 2), "0000-1700"), - (datetime.date(2024, 8, 3), None), - (datetime.date(2024, 8, 4), "1700-0000"), - (datetime.date(2024, 8, 5), "0000-0000"), - (datetime.date(2024, 8, 6), "0000-0000"), - (datetime.date(2024, 8, 7), "0000-0000"), - (datetime.date(2024, 8, 8), "0000-0000"), - (datetime.date(2024, 8, 9), "0000-1700"), - (datetime.date(2024, 8, 10), None), - (datetime.date(2024, 8, 11), "1700-0000"), - (datetime.date(2024, 8, 12), "0000-0000"), - (datetime.date(2024, 8, 13), "0000-0000"), - (datetime.date(2024, 8, 14), "0000-0000"), - (datetime.date(2024, 8, 15), "0000-0000"), - (datetime.date(2024, 8, 16), "0000-1700"), - (datetime.date(2024, 8, 17), None), - (datetime.date(2024, 8, 18), "1700-0000"), - (datetime.date(2024, 8, 19), "0000-0000"), - (datetime.date(2024, 8, 20), "0000-0000"), - (datetime.date(2024, 8, 21), "0000-0000"), - (datetime.date(2024, 8, 22), "0000-0000"), - (datetime.date(2024, 8, 23), "0000-1700"), - (datetime.date(2024, 8, 24), None), - (datetime.date(2024, 8, 25), "1700-0000"), - (datetime.date(2024, 8, 26), "0000-0000"), - (datetime.date(2024, 8, 27), "0000-0000"), - (datetime.date(2024, 8, 28), "0000-0000"), - (datetime.date(2024, 8, 29), "0000-0000"), - (datetime.date(2024, 8, 30), "0000-1700"), - (datetime.date(2024, 8, 31), None), - (datetime.date(2024, 9, 1), "1700-0000"), - (datetime.date(2024, 9, 2), "0000-0000"), - (datetime.date(2024, 9, 3), "0000-0000"), - (datetime.date(2024, 9, 4), "0000-0000"), - (datetime.date(2024, 9, 5), "0000-0000"), - (datetime.date(2024, 9, 6), "0000-1700"), - (datetime.date(2024, 9, 7), None), - (datetime.date(2024, 9, 8), "1700-0000"), - (datetime.date(2024, 9, 9), "0000-0000"), - (datetime.date(2024, 9, 10), "0000-0000"), - (datetime.date(2024, 9, 11), "0000-0000"), - (datetime.date(2024, 9, 12), "0000-0000"), - (datetime.date(2024, 9, 13), "0000-1700"), - (datetime.date(2024, 9, 14), None), - (datetime.date(2024, 9, 15), "1700-0000"), - (datetime.date(2024, 9, 16), "0000-0000"), - (datetime.date(2024, 9, 17), "0000-0000"), - (datetime.date(2024, 9, 18), "0000-0000"), - (datetime.date(2024, 9, 19), "0000-0000"), - (datetime.date(2024, 9, 20), "0000-1700"), - (datetime.date(2024, 9, 21), None), - (datetime.date(2024, 9, 22), "1700-0000"), - (datetime.date(2024, 9, 23), "0000-0000"), - (datetime.date(2024, 9, 24), "0000-0000"), - (datetime.date(2024, 9, 25), "0000-0000"), - (datetime.date(2024, 9, 26), "0000-0000"), - (datetime.date(2024, 9, 27), "0000-1700"), - (datetime.date(2024, 9, 28), None), - (datetime.date(2024, 9, 29), "1700-0000"), - (datetime.date(2024, 9, 30), "0000-0000"), - (datetime.date(2024, 10, 1), "0000-0000"), - (datetime.date(2024, 10, 2), "0000-0000"), - (datetime.date(2024, 10, 3), "0000-0000"), - (datetime.date(2024, 10, 4), "0000-1700"), - (datetime.date(2024, 10, 5), None), - (datetime.date(2024, 10, 6), "1700-0000"), - (datetime.date(2024, 10, 7), "0000-0000"), - (datetime.date(2024, 10, 8), "0000-0000"), - (datetime.date(2024, 10, 9), "0000-0000"), - (datetime.date(2024, 10, 10), "0000-0000"), - (datetime.date(2024, 10, 11), "0000-1700"), - (datetime.date(2024, 10, 12), None), - (datetime.date(2024, 10, 13), "1700-0000"), - (datetime.date(2024, 10, 14), "0000-0000"), - (datetime.date(2024, 10, 15), "0000-0000"), - (datetime.date(2024, 10, 16), "0000-0000"), - (datetime.date(2024, 10, 17), "0000-0000"), - (datetime.date(2024, 10, 18), "0000-1700"), - (datetime.date(2024, 10, 19), None), - (datetime.date(2024, 10, 20), "1700-0000"), - (datetime.date(2024, 10, 21), "0000-0000"), - (datetime.date(2024, 10, 22), "0000-0000"), - (datetime.date(2024, 10, 23), "0000-0000"), - (datetime.date(2024, 10, 24), "0000-0000"), - (datetime.date(2024, 10, 25), "0000-1700"), - (datetime.date(2024, 10, 26), None), - (datetime.date(2024, 10, 27), "1700-0000"), - (datetime.date(2024, 10, 28), "0000-0000"), - (datetime.date(2024, 10, 29), "0000-0000"), - (datetime.date(2024, 10, 30), "0000-0000"), - (datetime.date(2024, 10, 31), "0000-0000"), - (datetime.date(2024, 11, 1), "0000-1700"), - (datetime.date(2024, 11, 2), None), - (datetime.date(2024, 11, 3), "1700-0000"), - (datetime.date(2024, 11, 4), "0000-0000"), - (datetime.date(2024, 11, 5), "0000-0000"), - (datetime.date(2024, 11, 6), "0000-0000"), - (datetime.date(2024, 11, 7), "0000-0000"), - (datetime.date(2024, 11, 8), "0000-1700"), - (datetime.date(2024, 11, 9), None), - (datetime.date(2024, 11, 10), "1700-0000"), - (datetime.date(2024, 11, 11), "0000-0000"), - (datetime.date(2024, 11, 12), "0000-0000"), - (datetime.date(2024, 11, 13), "0000-0000"), - (datetime.date(2024, 11, 14), "0000-0000"), - (datetime.date(2024, 11, 15), "0000-1700"), - (datetime.date(2024, 11, 16), None), - (datetime.date(2024, 11, 17), "1700-0000"), - (datetime.date(2024, 11, 18), "0000-0000"), - (datetime.date(2024, 11, 19), "0000-0000"), - (datetime.date(2024, 11, 20), "0000-0000"), - (datetime.date(2024, 11, 21), "0000-0000"), - (datetime.date(2024, 11, 22), "0000-1700"), - (datetime.date(2024, 11, 23), None), - (datetime.date(2024, 11, 24), "1700-0000"), - (datetime.date(2024, 11, 25), "0000-0000"), - (datetime.date(2024, 11, 26), "0000-0000"), - (datetime.date(2024, 11, 27), "0000-0000"), - (datetime.date(2024, 11, 28), "0000-0000"), - (datetime.date(2024, 11, 29), "0000-1700"), - (datetime.date(2024, 11, 30), None), - (datetime.date(2024, 12, 1), "1700-0000"), - (datetime.date(2024, 12, 2), "0000-0000"), - (datetime.date(2024, 12, 3), "0000-0000"), - (datetime.date(2024, 12, 4), "0000-0000"), - (datetime.date(2024, 12, 5), "0000-0000"), - (datetime.date(2024, 12, 6), "0000-1700"), - (datetime.date(2024, 12, 7), None), - (datetime.date(2024, 12, 8), "1700-0000"), - (datetime.date(2024, 12, 9), "0000-0000"), - (datetime.date(2024, 12, 10), "0000-0000"), - (datetime.date(2024, 12, 11), "0000-0000"), - (datetime.date(2024, 12, 12), "0000-0000"), - (datetime.date(2024, 12, 13), "0000-1700"), - (datetime.date(2024, 12, 14), None), - (datetime.date(2024, 12, 15), "1700-0000"), - (datetime.date(2024, 12, 16), "0000-0000"), - (datetime.date(2024, 12, 17), "0000-0000"), - (datetime.date(2024, 12, 18), "0000-0000"), - (datetime.date(2024, 12, 19), "0000-0000"), - (datetime.date(2024, 12, 20), "0000-1700"), - (datetime.date(2024, 12, 21), None), - (datetime.date(2024, 12, 22), "1700-0000"), - (datetime.date(2024, 12, 23), "0000-0000"), - (datetime.date(2024, 12, 24), "0000-1700"), - (datetime.date(2024, 12, 25), "1700-0000"), - (datetime.date(2024, 12, 26), "0000-0000"), - (datetime.date(2024, 12, 27), "0000-1700"), - (datetime.date(2024, 12, 28), None), - (datetime.date(2024, 12, 29), "1700-0000"), - (datetime.date(2024, 12, 30), "0000-0000"), - (datetime.date(2024, 12, 31), "0000-0000"), -] - -METAL_2024_INTERVALS = [ - (datetime.date(2023, 12, 31), None), - (datetime.date(2024, 1, 1), "1700-0000"), - (datetime.date(2024, 1, 2), "0000-0000"), - (datetime.date(2024, 1, 3), "0000-0000"), - (datetime.date(2024, 1, 4), "0000-0000"), - (datetime.date(2024, 1, 5), "0000-1700"), - (datetime.date(2024, 1, 6), None), - (datetime.date(2024, 1, 7), "1700-0000"), - (datetime.date(2024, 1, 8), "0000-0000"), - (datetime.date(2024, 1, 9), "0000-0000"), - (datetime.date(2024, 1, 10), "0000-0000"), - (datetime.date(2024, 1, 11), "0000-0000"), - (datetime.date(2024, 1, 12), "0000-1700"), - (datetime.date(2024, 1, 13), None), - (datetime.date(2024, 1, 14), "1700-0000"), - (datetime.date(2024, 1, 15), "0000-1430"), - (datetime.date(2024, 1, 15), "1800-0000"), - (datetime.date(2024, 1, 16), "0000-0000"), - (datetime.date(2024, 1, 17), "0000-0000"), - (datetime.date(2024, 1, 18), "0000-0000"), - (datetime.date(2024, 1, 19), "0000-1700"), - (datetime.date(2024, 1, 20), None), - (datetime.date(2024, 1, 21), "1700-0000"), - (datetime.date(2024, 1, 22), "0000-0000"), - (datetime.date(2024, 1, 23), "0000-0000"), - (datetime.date(2024, 1, 24), "0000-0000"), - (datetime.date(2024, 1, 25), "0000-0000"), - (datetime.date(2024, 1, 26), "0000-1700"), - (datetime.date(2024, 1, 27), None), - (datetime.date(2024, 1, 28), "1700-0000"), - (datetime.date(2024, 1, 29), "0000-0000"), - (datetime.date(2024, 1, 30), "0000-0000"), - (datetime.date(2024, 1, 31), "0000-0000"), - (datetime.date(2024, 2, 1), "0000-0000"), - (datetime.date(2024, 2, 2), "0000-1700"), - (datetime.date(2024, 2, 3), None), - (datetime.date(2024, 2, 4), "1700-0000"), - (datetime.date(2024, 2, 5), "0000-0000"), - (datetime.date(2024, 2, 6), "0000-0000"), - (datetime.date(2024, 2, 7), "0000-0000"), - (datetime.date(2024, 2, 8), "0000-0000"), - (datetime.date(2024, 2, 9), "0000-1700"), - (datetime.date(2024, 2, 10), None), - (datetime.date(2024, 2, 11), "1700-0000"), - (datetime.date(2024, 2, 12), "0000-0000"), - (datetime.date(2024, 2, 13), "0000-0000"), - (datetime.date(2024, 2, 14), "0000-0000"), - (datetime.date(2024, 2, 15), "0000-0000"), - (datetime.date(2024, 2, 16), "0000-1700"), - (datetime.date(2024, 2, 17), None), - (datetime.date(2024, 2, 18), "1700-0000"), - (datetime.date(2024, 2, 19), "0000-1430"), - (datetime.date(2024, 2, 19), "1800-0000"), - (datetime.date(2024, 2, 20), "0000-0000"), - (datetime.date(2024, 2, 21), "0000-0000"), - (datetime.date(2024, 2, 22), "0000-0000"), - (datetime.date(2024, 2, 23), "0000-1700"), - (datetime.date(2024, 2, 24), None), - (datetime.date(2024, 2, 25), "1700-0000"), - (datetime.date(2024, 2, 26), "0000-0000"), - (datetime.date(2024, 2, 27), "0000-0000"), - (datetime.date(2024, 2, 28), "0000-0000"), - (datetime.date(2024, 2, 29), "0000-0000"), - (datetime.date(2024, 3, 1), "0000-1700"), - (datetime.date(2024, 3, 2), None), - (datetime.date(2024, 3, 3), "1700-0000"), - (datetime.date(2024, 3, 4), "0000-0000"), - (datetime.date(2024, 3, 5), "0000-0000"), - (datetime.date(2024, 3, 6), "0000-0000"), - (datetime.date(2024, 3, 7), "0000-0000"), - (datetime.date(2024, 3, 8), "0000-1700"), - (datetime.date(2024, 3, 9), None), - (datetime.date(2024, 3, 10), "1700-0000"), - (datetime.date(2024, 3, 11), "0000-0000"), - (datetime.date(2024, 3, 12), "0000-0000"), - (datetime.date(2024, 3, 13), "0000-0000"), - (datetime.date(2024, 3, 14), "0000-0000"), - (datetime.date(2024, 3, 15), "0000-1700"), - (datetime.date(2024, 3, 16), None), - (datetime.date(2024, 3, 17), "1700-0000"), - (datetime.date(2024, 3, 18), "0000-0000"), - (datetime.date(2024, 3, 19), "0000-0000"), - (datetime.date(2024, 3, 20), "0000-0000"), - (datetime.date(2024, 3, 21), "0000-0000"), - (datetime.date(2024, 3, 22), "0000-1700"), - (datetime.date(2024, 3, 23), None), - (datetime.date(2024, 3, 24), "1700-0000"), - (datetime.date(2024, 3, 25), "0000-0000"), - (datetime.date(2024, 3, 26), "0000-0000"), - (datetime.date(2024, 3, 27), "0000-0000"), - (datetime.date(2024, 3, 28), "0000-0000"), - (datetime.date(2024, 3, 29), "0000-1700"), - (datetime.date(2024, 3, 30), None), - (datetime.date(2024, 3, 31), "1700-0000"), - (datetime.date(2024, 4, 1), "0000-0000"), - (datetime.date(2024, 4, 2), "0000-0000"), - (datetime.date(2024, 4, 3), "0000-0000"), - (datetime.date(2024, 4, 4), "0000-0000"), - (datetime.date(2024, 4, 5), "0000-1700"), - (datetime.date(2024, 4, 6), None), - (datetime.date(2024, 4, 7), "1700-0000"), - (datetime.date(2024, 4, 8), "0000-0000"), - (datetime.date(2024, 4, 9), "0000-0000"), - (datetime.date(2024, 4, 10), "0000-0000"), - (datetime.date(2024, 4, 11), "0000-0000"), - (datetime.date(2024, 4, 12), "0000-1700"), - (datetime.date(2024, 4, 13), None), - (datetime.date(2024, 4, 14), "1700-0000"), - (datetime.date(2024, 4, 15), "0000-0000"), - (datetime.date(2024, 4, 16), "0000-0000"), - (datetime.date(2024, 4, 17), "0000-0000"), - (datetime.date(2024, 4, 18), "0000-0000"), - (datetime.date(2024, 4, 19), "0000-1700"), - (datetime.date(2024, 4, 20), None), - (datetime.date(2024, 4, 21), "1700-0000"), - (datetime.date(2024, 4, 22), "0000-0000"), - (datetime.date(2024, 4, 23), "0000-0000"), - (datetime.date(2024, 4, 24), "0000-0000"), - (datetime.date(2024, 4, 25), "0000-0000"), - (datetime.date(2024, 4, 26), "0000-1700"), - (datetime.date(2024, 4, 27), None), - (datetime.date(2024, 4, 28), "1700-0000"), - (datetime.date(2024, 4, 29), "0000-0000"), - (datetime.date(2024, 4, 30), "0000-0000"), - (datetime.date(2024, 5, 1), "0000-0000"), - (datetime.date(2024, 5, 2), "0000-0000"), - (datetime.date(2024, 5, 3), "0000-1700"), - (datetime.date(2024, 5, 4), None), - (datetime.date(2024, 5, 5), "1700-0000"), - (datetime.date(2024, 5, 6), "0000-0000"), - (datetime.date(2024, 5, 7), "0000-0000"), - (datetime.date(2024, 5, 8), "0000-0000"), - (datetime.date(2024, 5, 9), "0000-0000"), - (datetime.date(2024, 5, 10), "0000-1700"), - (datetime.date(2024, 5, 11), None), - (datetime.date(2024, 5, 12), "1700-0000"), - (datetime.date(2024, 5, 13), "0000-0000"), - (datetime.date(2024, 5, 14), "0000-0000"), - (datetime.date(2024, 5, 15), "0000-0000"), - (datetime.date(2024, 5, 16), "0000-0000"), - (datetime.date(2024, 5, 17), "0000-1700"), - (datetime.date(2024, 5, 18), None), - (datetime.date(2024, 5, 19), "1700-0000"), - (datetime.date(2024, 5, 20), "0000-0000"), - (datetime.date(2024, 5, 21), "0000-0000"), - (datetime.date(2024, 5, 22), "0000-0000"), - (datetime.date(2024, 5, 23), "0000-0000"), - (datetime.date(2024, 5, 24), "0000-1700"), - (datetime.date(2024, 5, 25), None), - (datetime.date(2024, 5, 26), "1700-0000"), - (datetime.date(2024, 5, 27), "0000-0000"), - (datetime.date(2024, 5, 28), "0000-0000"), - (datetime.date(2024, 5, 29), "0000-0000"), - (datetime.date(2024, 5, 30), "0000-0000"), - (datetime.date(2024, 5, 31), "0000-1700"), - (datetime.date(2024, 6, 1), None), - (datetime.date(2024, 6, 2), "1700-0000"), - (datetime.date(2024, 6, 3), "0000-0000"), - (datetime.date(2024, 6, 4), "0000-0000"), - (datetime.date(2024, 6, 5), "0000-0000"), - (datetime.date(2024, 6, 6), "0000-0000"), - (datetime.date(2024, 6, 7), "0000-1700"), - (datetime.date(2024, 6, 8), None), - (datetime.date(2024, 6, 9), "1700-0000"), - (datetime.date(2024, 6, 10), "0000-0000"), - (datetime.date(2024, 6, 11), "0000-0000"), - (datetime.date(2024, 6, 12), "0000-0000"), - (datetime.date(2024, 6, 13), "0000-0000"), - (datetime.date(2024, 6, 14), "0000-1700"), - (datetime.date(2024, 6, 15), None), - (datetime.date(2024, 6, 16), "1700-0000"), - (datetime.date(2024, 6, 17), "0000-0000"), - (datetime.date(2024, 6, 18), "0000-0000"), - (datetime.date(2024, 6, 19), "0000-0000"), - (datetime.date(2024, 6, 20), "0000-0000"), - (datetime.date(2024, 6, 21), "0000-1700"), - (datetime.date(2024, 6, 22), None), - (datetime.date(2024, 6, 23), "1700-0000"), - (datetime.date(2024, 6, 24), "0000-0000"), - (datetime.date(2024, 6, 25), "0000-0000"), - (datetime.date(2024, 6, 26), "0000-0000"), - (datetime.date(2024, 6, 27), "0000-0000"), - (datetime.date(2024, 6, 28), "0000-1700"), - (datetime.date(2024, 6, 29), None), - (datetime.date(2024, 6, 30), "1700-0000"), - (datetime.date(2024, 7, 1), "0000-0000"), - (datetime.date(2024, 7, 2), "0000-0000"), - (datetime.date(2024, 7, 3), "0000-0000"), - (datetime.date(2024, 7, 4), "0000-0000"), - (datetime.date(2024, 7, 5), "0000-1700"), - (datetime.date(2024, 7, 6), None), - (datetime.date(2024, 7, 7), "1700-0000"), - (datetime.date(2024, 7, 8), "0000-0000"), - (datetime.date(2024, 7, 9), "0000-0000"), - (datetime.date(2024, 7, 10), "0000-0000"), - (datetime.date(2024, 7, 11), "0000-0000"), - (datetime.date(2024, 7, 12), "0000-1700"), - (datetime.date(2024, 7, 13), None), - (datetime.date(2024, 7, 14), "1700-0000"), - (datetime.date(2024, 7, 15), "0000-0000"), - (datetime.date(2024, 7, 16), "0000-0000"), - (datetime.date(2024, 7, 17), "0000-0000"), - (datetime.date(2024, 7, 18), "0000-0000"), - (datetime.date(2024, 7, 19), "0000-1700"), - (datetime.date(2024, 7, 20), None), - (datetime.date(2024, 7, 21), "1700-0000"), - (datetime.date(2024, 7, 22), "0000-0000"), - (datetime.date(2024, 7, 23), "0000-0000"), - (datetime.date(2024, 7, 24), "0000-0000"), - (datetime.date(2024, 7, 25), "0000-0000"), - (datetime.date(2024, 7, 26), "0000-1700"), - (datetime.date(2024, 7, 27), None), - (datetime.date(2024, 7, 28), "1700-0000"), - (datetime.date(2024, 7, 29), "0000-0000"), - (datetime.date(2024, 7, 30), "0000-0000"), - (datetime.date(2024, 7, 31), "0000-0000"), - (datetime.date(2024, 8, 1), "0000-0000"), - (datetime.date(2024, 8, 2), "0000-1700"), - (datetime.date(2024, 8, 3), None), - (datetime.date(2024, 8, 4), "1700-0000"), - (datetime.date(2024, 8, 5), "0000-0000"), - (datetime.date(2024, 8, 6), "0000-0000"), - (datetime.date(2024, 8, 7), "0000-0000"), - (datetime.date(2024, 8, 8), "0000-0000"), - (datetime.date(2024, 8, 9), "0000-1700"), - (datetime.date(2024, 8, 10), None), - (datetime.date(2024, 8, 11), "1700-0000"), - (datetime.date(2024, 8, 12), "0000-0000"), - (datetime.date(2024, 8, 13), "0000-0000"), - (datetime.date(2024, 8, 14), "0000-0000"), - (datetime.date(2024, 8, 15), "0000-0000"), - (datetime.date(2024, 8, 16), "0000-1700"), - (datetime.date(2024, 8, 17), None), - (datetime.date(2024, 8, 18), "1700-0000"), - (datetime.date(2024, 8, 19), "0000-0000"), - (datetime.date(2024, 8, 20), "0000-0000"), - (datetime.date(2024, 8, 21), "0000-0000"), - (datetime.date(2024, 8, 22), "0000-0000"), - (datetime.date(2024, 8, 23), "0000-1700"), - (datetime.date(2024, 8, 24), None), - (datetime.date(2024, 8, 25), "1700-0000"), - (datetime.date(2024, 8, 26), "0000-0000"), - (datetime.date(2024, 8, 27), "0000-0000"), - (datetime.date(2024, 8, 28), "0000-0000"), - (datetime.date(2024, 8, 29), "0000-0000"), - (datetime.date(2024, 8, 30), "0000-1700"), - (datetime.date(2024, 8, 31), None), - (datetime.date(2024, 9, 1), "1700-0000"), - (datetime.date(2024, 9, 2), "0000-0000"), - (datetime.date(2024, 9, 3), "0000-0000"), - (datetime.date(2024, 9, 4), "0000-0000"), - (datetime.date(2024, 9, 5), "0000-0000"), - (datetime.date(2024, 9, 6), "0000-1700"), - (datetime.date(2024, 9, 7), None), - (datetime.date(2024, 9, 8), "1700-0000"), - (datetime.date(2024, 9, 9), "0000-0000"), - (datetime.date(2024, 9, 10), "0000-0000"), - (datetime.date(2024, 9, 11), "0000-0000"), - (datetime.date(2024, 9, 12), "0000-0000"), - (datetime.date(2024, 9, 13), "0000-1700"), - (datetime.date(2024, 9, 14), None), - (datetime.date(2024, 9, 15), "1700-0000"), - (datetime.date(2024, 9, 16), "0000-0000"), - (datetime.date(2024, 9, 17), "0000-0000"), - (datetime.date(2024, 9, 18), "0000-0000"), - (datetime.date(2024, 9, 19), "0000-0000"), - (datetime.date(2024, 9, 20), "0000-1700"), - (datetime.date(2024, 9, 21), None), - (datetime.date(2024, 9, 22), "1700-0000"), - (datetime.date(2024, 9, 23), "0000-0000"), - (datetime.date(2024, 9, 24), "0000-0000"), - (datetime.date(2024, 9, 25), "0000-0000"), - (datetime.date(2024, 9, 26), "0000-0000"), - (datetime.date(2024, 9, 27), "0000-1700"), - (datetime.date(2024, 9, 28), None), - (datetime.date(2024, 9, 29), "1700-0000"), - (datetime.date(2024, 9, 30), "0000-0000"), - (datetime.date(2024, 10, 1), "0000-0000"), - (datetime.date(2024, 10, 2), "0000-0000"), - (datetime.date(2024, 10, 3), "0000-0000"), - (datetime.date(2024, 10, 4), "0000-1700"), - (datetime.date(2024, 10, 5), None), - (datetime.date(2024, 10, 6), "1700-0000"), - (datetime.date(2024, 10, 7), "0000-0000"), - (datetime.date(2024, 10, 8), "0000-0000"), - (datetime.date(2024, 10, 9), "0000-0000"), - (datetime.date(2024, 10, 10), "0000-0000"), - (datetime.date(2024, 10, 11), "0000-1700"), - (datetime.date(2024, 10, 12), None), - (datetime.date(2024, 10, 13), "1700-0000"), - (datetime.date(2024, 10, 14), "0000-0000"), - (datetime.date(2024, 10, 15), "0000-0000"), - (datetime.date(2024, 10, 16), "0000-0000"), - (datetime.date(2024, 10, 17), "0000-0000"), - (datetime.date(2024, 10, 18), "0000-1700"), - (datetime.date(2024, 10, 19), None), - (datetime.date(2024, 10, 20), "1700-0000"), - (datetime.date(2024, 10, 21), "0000-0000"), - (datetime.date(2024, 10, 22), "0000-0000"), - (datetime.date(2024, 10, 23), "0000-0000"), - (datetime.date(2024, 10, 24), "0000-0000"), - (datetime.date(2024, 10, 25), "0000-1700"), - (datetime.date(2024, 10, 26), None), - (datetime.date(2024, 10, 27), "1700-0000"), - (datetime.date(2024, 10, 28), "0000-0000"), - (datetime.date(2024, 10, 29), "0000-0000"), - (datetime.date(2024, 10, 30), "0000-0000"), - (datetime.date(2024, 10, 31), "0000-0000"), - (datetime.date(2024, 11, 1), "0000-1700"), - (datetime.date(2024, 11, 2), None), - (datetime.date(2024, 11, 3), "1700-0000"), - (datetime.date(2024, 11, 4), "0000-0000"), - (datetime.date(2024, 11, 5), "0000-0000"), - (datetime.date(2024, 11, 6), "0000-0000"), - (datetime.date(2024, 11, 7), "0000-0000"), - (datetime.date(2024, 11, 8), "0000-1700"), - (datetime.date(2024, 11, 9), None), - (datetime.date(2024, 11, 10), "1700-0000"), - (datetime.date(2024, 11, 11), "0000-0000"), - (datetime.date(2024, 11, 12), "0000-0000"), - (datetime.date(2024, 11, 13), "0000-0000"), - (datetime.date(2024, 11, 14), "0000-0000"), - (datetime.date(2024, 11, 15), "0000-1700"), - (datetime.date(2024, 11, 16), None), - (datetime.date(2024, 11, 17), "1700-0000"), - (datetime.date(2024, 11, 18), "0000-0000"), - (datetime.date(2024, 11, 19), "0000-0000"), - (datetime.date(2024, 11, 20), "0000-0000"), - (datetime.date(2024, 11, 21), "0000-0000"), - (datetime.date(2024, 11, 22), "0000-1700"), - (datetime.date(2024, 11, 23), None), - (datetime.date(2024, 11, 24), "1700-0000"), - (datetime.date(2024, 11, 25), "0000-0000"), - (datetime.date(2024, 11, 26), "0000-0000"), - (datetime.date(2024, 11, 27), "0000-0000"), - (datetime.date(2024, 11, 28), "0000-0000"), - (datetime.date(2024, 11, 29), "0000-1700"), - (datetime.date(2024, 11, 30), None), - (datetime.date(2024, 12, 1), "1700-0000"), - (datetime.date(2024, 12, 2), "0000-0000"), - (datetime.date(2024, 12, 3), "0000-0000"), - (datetime.date(2024, 12, 4), "0000-0000"), - (datetime.date(2024, 12, 5), "0000-0000"), - (datetime.date(2024, 12, 6), "0000-1700"), - (datetime.date(2024, 12, 7), None), - (datetime.date(2024, 12, 8), "1700-0000"), - (datetime.date(2024, 12, 9), "0000-0000"), - (datetime.date(2024, 12, 10), "0000-0000"), - (datetime.date(2024, 12, 11), "0000-0000"), - (datetime.date(2024, 12, 12), "0000-0000"), - (datetime.date(2024, 12, 13), "0000-1700"), - (datetime.date(2024, 12, 14), None), - (datetime.date(2024, 12, 15), "1700-0000"), - (datetime.date(2024, 12, 16), "0000-0000"), - (datetime.date(2024, 12, 17), "0000-0000"), - (datetime.date(2024, 12, 18), "0000-0000"), - (datetime.date(2024, 12, 19), "0000-0000"), - (datetime.date(2024, 12, 20), "0000-1700"), - (datetime.date(2024, 12, 21), None), - (datetime.date(2024, 12, 22), "1700-0000"), - (datetime.date(2024, 12, 23), "0000-0000"), - (datetime.date(2024, 12, 24), "0000-1700"), - (datetime.date(2024, 12, 25), "1700-0000"), - (datetime.date(2024, 12, 26), "0000-0000"), - (datetime.date(2024, 12, 27), "0000-1700"), - (datetime.date(2024, 12, 28), None), - (datetime.date(2024, 12, 29), "1700-0000"), - (datetime.date(2024, 12, 30), "0000-0000"), - (datetime.date(2024, 12, 31), "0000-0000"), -] - -RATES_2024_INTERVALS = [ - (datetime.date(2024, 1, 1), None), - (datetime.date(2024, 1, 2), "0800-1700"), - (datetime.date(2024, 1, 3), "0800-1700"), - (datetime.date(2024, 1, 4), "0800-1700"), - (datetime.date(2024, 1, 5), "0800-1700"), - (datetime.date(2024, 1, 6), None), - (datetime.date(2024, 1, 7), None), - (datetime.date(2024, 1, 8), "0800-1700"), - (datetime.date(2024, 1, 9), "0800-1700"), - (datetime.date(2024, 1, 10), "0800-1700"), - (datetime.date(2024, 1, 11), "0800-1700"), - (datetime.date(2024, 1, 12), "0800-1700"), - (datetime.date(2024, 1, 13), None), - (datetime.date(2024, 1, 14), None), - (datetime.date(2024, 1, 15), None), - (datetime.date(2024, 1, 16), "0800-1700"), - (datetime.date(2024, 1, 17), "0800-1700"), - (datetime.date(2024, 1, 18), "0800-1700"), - (datetime.date(2024, 1, 19), "0800-1700"), - (datetime.date(2024, 1, 20), None), - (datetime.date(2024, 1, 21), None), - (datetime.date(2024, 1, 22), "0800-1700"), - (datetime.date(2024, 1, 23), "0800-1700"), - (datetime.date(2024, 1, 24), "0800-1700"), - (datetime.date(2024, 1, 25), "0800-1700"), - (datetime.date(2024, 1, 26), "0800-1700"), - (datetime.date(2024, 1, 27), None), - (datetime.date(2024, 1, 28), None), - (datetime.date(2024, 1, 29), "0800-1700"), - (datetime.date(2024, 1, 30), "0800-1700"), - (datetime.date(2024, 1, 31), "0800-1700"), - (datetime.date(2024, 2, 1), "0800-1700"), - (datetime.date(2024, 2, 2), "0800-1700"), - (datetime.date(2024, 2, 3), None), - (datetime.date(2024, 2, 4), None), - (datetime.date(2024, 2, 5), "0800-1700"), - (datetime.date(2024, 2, 6), "0800-1700"), - (datetime.date(2024, 2, 7), "0800-1700"), - (datetime.date(2024, 2, 8), "0800-1700"), - (datetime.date(2024, 2, 9), "0800-1700"), - (datetime.date(2024, 2, 10), None), - (datetime.date(2024, 2, 11), None), - (datetime.date(2024, 2, 12), "0800-1700"), - (datetime.date(2024, 2, 13), "0800-1700"), - (datetime.date(2024, 2, 14), "0800-1700"), - (datetime.date(2024, 2, 15), "0800-1700"), - (datetime.date(2024, 2, 16), "0800-1700"), - (datetime.date(2024, 2, 17), None), - (datetime.date(2024, 2, 18), None), - (datetime.date(2024, 2, 19), None), - (datetime.date(2024, 2, 20), "0800-1700"), - (datetime.date(2024, 2, 21), "0800-1700"), - (datetime.date(2024, 2, 22), "0800-1700"), - (datetime.date(2024, 2, 23), "0800-1700"), - (datetime.date(2024, 2, 24), None), - (datetime.date(2024, 2, 25), None), - (datetime.date(2024, 2, 26), "0800-1700"), - (datetime.date(2024, 2, 27), "0800-1700"), - (datetime.date(2024, 2, 28), "0800-1700"), - (datetime.date(2024, 2, 29), "0800-1700"), - (datetime.date(2024, 3, 1), "0800-1700"), - (datetime.date(2024, 3, 2), None), - (datetime.date(2024, 3, 3), None), - (datetime.date(2024, 3, 4), "0800-1700"), - (datetime.date(2024, 3, 5), "0800-1700"), - (datetime.date(2024, 3, 6), "0800-1700"), - (datetime.date(2024, 3, 7), "0800-1700"), - (datetime.date(2024, 3, 8), "0800-1700"), - (datetime.date(2024, 3, 9), None), - (datetime.date(2024, 3, 10), None), - (datetime.date(2024, 3, 11), "0800-1700"), - (datetime.date(2024, 3, 12), "0800-1700"), - (datetime.date(2024, 3, 13), "0800-1700"), - (datetime.date(2024, 3, 14), "0800-1700"), - (datetime.date(2024, 3, 15), "0800-1700"), - (datetime.date(2024, 3, 16), None), - (datetime.date(2024, 3, 17), None), - (datetime.date(2024, 3, 18), "0800-1700"), - (datetime.date(2024, 3, 19), "0800-1700"), - (datetime.date(2024, 3, 20), "0800-1700"), - (datetime.date(2024, 3, 21), "0800-1700"), - (datetime.date(2024, 3, 22), "0800-1700"), - (datetime.date(2024, 3, 23), None), - (datetime.date(2024, 3, 24), None), - (datetime.date(2024, 3, 25), "0800-1700"), - (datetime.date(2024, 3, 26), "0800-1700"), - (datetime.date(2024, 3, 27), "0800-1700"), - (datetime.date(2024, 3, 28), "0800-1700"), - (datetime.date(2024, 3, 29), None), - (datetime.date(2024, 3, 30), None), - (datetime.date(2024, 3, 31), None), - (datetime.date(2024, 4, 1), "0800-1700"), - (datetime.date(2024, 4, 2), "0800-1700"), - (datetime.date(2024, 4, 3), "0800-1700"), - (datetime.date(2024, 4, 4), "0800-1700"), - (datetime.date(2024, 4, 5), "0800-1700"), - (datetime.date(2024, 4, 6), None), - (datetime.date(2024, 4, 7), None), - (datetime.date(2024, 4, 8), "0800-1700"), - (datetime.date(2024, 4, 9), "0800-1700"), - (datetime.date(2024, 4, 10), "0800-1700"), - (datetime.date(2024, 4, 11), "0800-1700"), - (datetime.date(2024, 4, 12), "0800-1700"), - (datetime.date(2024, 4, 13), None), - (datetime.date(2024, 4, 14), None), - (datetime.date(2024, 4, 15), "0800-1700"), - (datetime.date(2024, 4, 16), "0800-1700"), - (datetime.date(2024, 4, 17), "0800-1700"), - (datetime.date(2024, 4, 18), "0800-1700"), - (datetime.date(2024, 4, 19), "0800-1700"), - (datetime.date(2024, 4, 20), None), - (datetime.date(2024, 4, 21), None), - (datetime.date(2024, 4, 22), "0800-1700"), - (datetime.date(2024, 4, 23), "0800-1700"), - (datetime.date(2024, 4, 24), "0800-1700"), - (datetime.date(2024, 4, 25), "0800-1700"), - (datetime.date(2024, 4, 26), "0800-1700"), - (datetime.date(2024, 4, 27), None), - (datetime.date(2024, 4, 28), None), - (datetime.date(2024, 4, 29), "0800-1700"), - (datetime.date(2024, 4, 30), "0800-1700"), - (datetime.date(2024, 5, 1), "0800-1700"), - (datetime.date(2024, 5, 2), "0800-1700"), - (datetime.date(2024, 5, 3), "0800-1700"), - (datetime.date(2024, 5, 4), None), - (datetime.date(2024, 5, 5), None), - (datetime.date(2024, 5, 6), "0800-1700"), - (datetime.date(2024, 5, 7), "0800-1700"), - (datetime.date(2024, 5, 8), "0800-1700"), - (datetime.date(2024, 5, 9), "0800-1700"), - (datetime.date(2024, 5, 10), "0800-1700"), - (datetime.date(2024, 5, 11), None), - (datetime.date(2024, 5, 12), None), - (datetime.date(2024, 5, 13), "0800-1700"), - (datetime.date(2024, 5, 14), "0800-1700"), - (datetime.date(2024, 5, 15), "0800-1700"), - (datetime.date(2024, 5, 16), "0800-1700"), - (datetime.date(2024, 5, 17), "0800-1700"), - (datetime.date(2024, 5, 18), None), - (datetime.date(2024, 5, 19), None), - (datetime.date(2024, 5, 20), "0800-1700"), - (datetime.date(2024, 5, 21), "0800-1700"), - (datetime.date(2024, 5, 22), "0800-1700"), - (datetime.date(2024, 5, 23), "0800-1700"), - (datetime.date(2024, 5, 24), "0800-1700"), - (datetime.date(2024, 5, 25), None), - (datetime.date(2024, 5, 26), None), - (datetime.date(2024, 5, 27), None), - (datetime.date(2024, 5, 28), "0800-1700"), - (datetime.date(2024, 5, 29), "0800-1700"), - (datetime.date(2024, 5, 30), "0800-1700"), - (datetime.date(2024, 5, 31), "0800-1700"), - (datetime.date(2024, 6, 1), None), - (datetime.date(2024, 6, 2), None), - (datetime.date(2024, 6, 3), "0800-1700"), - (datetime.date(2024, 6, 4), "0800-1700"), - (datetime.date(2024, 6, 5), "0800-1700"), - (datetime.date(2024, 6, 6), "0800-1700"), - (datetime.date(2024, 6, 7), "0800-1700"), - (datetime.date(2024, 6, 8), None), - (datetime.date(2024, 6, 9), None), - (datetime.date(2024, 6, 10), "0800-1700"), - (datetime.date(2024, 6, 11), "0800-1700"), - (datetime.date(2024, 6, 12), "0800-1700"), - (datetime.date(2024, 6, 13), "0800-1700"), - (datetime.date(2024, 6, 14), "0800-1700"), - (datetime.date(2024, 6, 15), None), - (datetime.date(2024, 6, 16), None), - (datetime.date(2024, 6, 17), "0800-1700"), - (datetime.date(2024, 6, 18), "0800-1700"), - (datetime.date(2024, 6, 19), None), - (datetime.date(2024, 6, 20), "0800-1700"), - (datetime.date(2024, 6, 21), "0800-1700"), - (datetime.date(2024, 6, 22), None), - (datetime.date(2024, 6, 23), None), - (datetime.date(2024, 6, 24), "0800-1700"), - (datetime.date(2024, 6, 25), "0800-1700"), - (datetime.date(2024, 6, 26), "0800-1700"), - (datetime.date(2024, 6, 27), "0800-1700"), - (datetime.date(2024, 6, 28), "0800-1700"), - (datetime.date(2024, 6, 29), None), - (datetime.date(2024, 6, 30), None), - (datetime.date(2024, 7, 1), "0800-1700"), - (datetime.date(2024, 7, 2), "0800-1700"), - (datetime.date(2024, 7, 3), "0800-1300"), - (datetime.date(2024, 7, 4), None), - (datetime.date(2024, 7, 5), "0800-1700"), - (datetime.date(2024, 7, 6), None), - (datetime.date(2024, 7, 7), None), - (datetime.date(2024, 7, 8), "0800-1700"), - (datetime.date(2024, 7, 9), "0800-1700"), - (datetime.date(2024, 7, 10), "0800-1700"), - (datetime.date(2024, 7, 11), "0800-1700"), - (datetime.date(2024, 7, 12), "0800-1700"), - (datetime.date(2024, 7, 13), None), - (datetime.date(2024, 7, 14), None), - (datetime.date(2024, 7, 15), "0800-1700"), - (datetime.date(2024, 7, 16), "0800-1700"), - (datetime.date(2024, 7, 17), "0800-1700"), - (datetime.date(2024, 7, 18), "0800-1700"), - (datetime.date(2024, 7, 19), "0800-1700"), - (datetime.date(2024, 7, 20), None), - (datetime.date(2024, 7, 21), None), - (datetime.date(2024, 7, 22), "0800-1700"), - (datetime.date(2024, 7, 23), "0800-1700"), - (datetime.date(2024, 7, 24), "0800-1700"), - (datetime.date(2024, 7, 25), "0800-1700"), - (datetime.date(2024, 7, 26), "0800-1700"), - (datetime.date(2024, 7, 27), None), - (datetime.date(2024, 7, 28), None), - (datetime.date(2024, 7, 29), "0800-1700"), - (datetime.date(2024, 7, 30), "0800-1700"), - (datetime.date(2024, 7, 31), "0800-1700"), - (datetime.date(2024, 8, 1), "0800-1700"), - (datetime.date(2024, 8, 2), "0800-1700"), - (datetime.date(2024, 8, 3), None), - (datetime.date(2024, 8, 4), None), - (datetime.date(2024, 8, 5), "0800-1700"), - (datetime.date(2024, 8, 6), "0800-1700"), - (datetime.date(2024, 8, 7), "0800-1700"), - (datetime.date(2024, 8, 8), "0800-1700"), - (datetime.date(2024, 8, 9), "0800-1700"), - (datetime.date(2024, 8, 10), None), - (datetime.date(2024, 8, 11), None), - (datetime.date(2024, 8, 12), "0800-1700"), - (datetime.date(2024, 8, 13), "0800-1700"), - (datetime.date(2024, 8, 14), "0800-1700"), - (datetime.date(2024, 8, 15), "0800-1700"), - (datetime.date(2024, 8, 16), "0800-1700"), - (datetime.date(2024, 8, 17), None), - (datetime.date(2024, 8, 18), None), - (datetime.date(2024, 8, 19), "0800-1700"), - (datetime.date(2024, 8, 20), "0800-1700"), - (datetime.date(2024, 8, 21), "0800-1700"), - (datetime.date(2024, 8, 22), "0800-1700"), - (datetime.date(2024, 8, 23), "0800-1700"), - (datetime.date(2024, 8, 24), None), - (datetime.date(2024, 8, 25), None), - (datetime.date(2024, 8, 26), "0800-1700"), - (datetime.date(2024, 8, 27), "0800-1700"), - (datetime.date(2024, 8, 28), "0800-1700"), - (datetime.date(2024, 8, 29), "0800-1700"), - (datetime.date(2024, 8, 30), "0800-1700"), - (datetime.date(2024, 8, 31), None), - (datetime.date(2024, 9, 1), None), - (datetime.date(2024, 9, 2), None), - (datetime.date(2024, 9, 3), "0800-1700"), - (datetime.date(2024, 9, 4), "0800-1700"), - (datetime.date(2024, 9, 5), "0800-1700"), - (datetime.date(2024, 9, 6), "0800-1700"), - (datetime.date(2024, 9, 7), None), - (datetime.date(2024, 9, 8), None), - (datetime.date(2024, 9, 9), "0800-1700"), - (datetime.date(2024, 9, 10), "0800-1700"), - (datetime.date(2024, 9, 11), "0800-1700"), - (datetime.date(2024, 9, 12), "0800-1700"), - (datetime.date(2024, 9, 13), "0800-1700"), - (datetime.date(2024, 9, 14), None), - (datetime.date(2024, 9, 15), None), - (datetime.date(2024, 9, 16), "0800-1700"), - (datetime.date(2024, 9, 17), "0800-1700"), - (datetime.date(2024, 9, 18), "0800-1700"), - (datetime.date(2024, 9, 19), "0800-1700"), - (datetime.date(2024, 9, 20), "0800-1700"), - (datetime.date(2024, 9, 21), None), - (datetime.date(2024, 9, 22), None), - (datetime.date(2024, 9, 23), "0800-1700"), - (datetime.date(2024, 9, 24), "0800-1700"), - (datetime.date(2024, 9, 25), "0800-1700"), - (datetime.date(2024, 9, 26), "0800-1700"), - (datetime.date(2024, 9, 27), "0800-1700"), - (datetime.date(2024, 9, 28), None), - (datetime.date(2024, 9, 29), None), - (datetime.date(2024, 9, 30), "0800-1700"), - (datetime.date(2024, 10, 1), "0800-1700"), - (datetime.date(2024, 10, 2), "0800-1700"), - (datetime.date(2024, 10, 3), "0800-1700"), - (datetime.date(2024, 10, 4), "0800-1700"), - (datetime.date(2024, 10, 5), None), - (datetime.date(2024, 10, 6), None), - (datetime.date(2024, 10, 7), "0800-1700"), - (datetime.date(2024, 10, 8), "0800-1700"), - (datetime.date(2024, 10, 9), "0800-1700"), - (datetime.date(2024, 10, 10), "0800-1700"), - (datetime.date(2024, 10, 11), "0800-1700"), - (datetime.date(2024, 10, 12), None), - (datetime.date(2024, 10, 13), None), - (datetime.date(2024, 10, 14), "0800-1700"), - (datetime.date(2024, 10, 15), "0800-1700"), - (datetime.date(2024, 10, 16), "0800-1700"), - (datetime.date(2024, 10, 17), "0800-1700"), - (datetime.date(2024, 10, 18), "0800-1700"), - (datetime.date(2024, 10, 19), None), - (datetime.date(2024, 10, 20), None), - (datetime.date(2024, 10, 21), "0800-1700"), - (datetime.date(2024, 10, 22), "0800-1700"), - (datetime.date(2024, 10, 23), "0800-1700"), - (datetime.date(2024, 10, 24), "0800-1700"), - (datetime.date(2024, 10, 25), "0800-1700"), - (datetime.date(2024, 10, 26), None), - (datetime.date(2024, 10, 27), None), - (datetime.date(2024, 10, 28), "0800-1700"), - (datetime.date(2024, 10, 29), "0800-1700"), - (datetime.date(2024, 10, 30), "0800-1700"), - (datetime.date(2024, 10, 31), "0800-1700"), - (datetime.date(2024, 11, 1), "0800-1700"), - (datetime.date(2024, 11, 2), None), - (datetime.date(2024, 11, 3), None), - (datetime.date(2024, 11, 4), "0800-1700"), - (datetime.date(2024, 11, 5), "0800-1700"), - (datetime.date(2024, 11, 6), "0800-1700"), - (datetime.date(2024, 11, 7), "0800-1700"), - (datetime.date(2024, 11, 8), "0800-1700"), - (datetime.date(2024, 11, 9), None), - (datetime.date(2024, 11, 10), None), - (datetime.date(2024, 11, 11), "0800-1700"), - (datetime.date(2024, 11, 12), "0800-1700"), - (datetime.date(2024, 11, 13), "0800-1700"), - (datetime.date(2024, 11, 14), "0800-1700"), - (datetime.date(2024, 11, 15), "0800-1700"), - (datetime.date(2024, 11, 16), None), - (datetime.date(2024, 11, 17), None), - (datetime.date(2024, 11, 18), "0800-1700"), - (datetime.date(2024, 11, 19), "0800-1700"), - (datetime.date(2024, 11, 20), "0800-1700"), - (datetime.date(2024, 11, 21), "0800-1700"), - (datetime.date(2024, 11, 22), "0800-1700"), - (datetime.date(2024, 11, 23), None), - (datetime.date(2024, 11, 24), None), - (datetime.date(2024, 11, 25), "0800-1700"), - (datetime.date(2024, 11, 26), "0800-1700"), - (datetime.date(2024, 11, 27), "0800-1700"), - (datetime.date(2024, 11, 28), None), - (datetime.date(2024, 11, 29), "0800-1300"), - (datetime.date(2024, 11, 30), None), - (datetime.date(2024, 12, 1), None), - (datetime.date(2024, 12, 2), "0800-1700"), - (datetime.date(2024, 12, 3), "0800-1700"), - (datetime.date(2024, 12, 4), "0800-1700"), - (datetime.date(2024, 12, 5), "0800-1700"), - (datetime.date(2024, 12, 6), "0800-1700"), - (datetime.date(2024, 12, 7), None), - (datetime.date(2024, 12, 8), None), - (datetime.date(2024, 12, 9), "0800-1700"), - (datetime.date(2024, 12, 10), "0800-1700"), - (datetime.date(2024, 12, 11), "0800-1700"), - (datetime.date(2024, 12, 12), "0800-1700"), - (datetime.date(2024, 12, 13), "0800-1700"), - (datetime.date(2024, 12, 14), None), - (datetime.date(2024, 12, 15), None), - (datetime.date(2024, 12, 16), "0800-1700"), - (datetime.date(2024, 12, 17), "0800-1700"), - (datetime.date(2024, 12, 18), "0800-1700"), - (datetime.date(2024, 12, 19), "0800-1700"), - (datetime.date(2024, 12, 20), "0800-1700"), - (datetime.date(2024, 12, 21), None), - (datetime.date(2024, 12, 22), None), - (datetime.date(2024, 12, 23), "0800-1700"), - (datetime.date(2024, 12, 24), "0800-1300"), - (datetime.date(2024, 12, 25), None), - (datetime.date(2024, 12, 26), "0800-1700"), - (datetime.date(2024, 12, 27), "0800-1700"), - (datetime.date(2024, 12, 28), None), - (datetime.date(2024, 12, 29), None), - (datetime.date(2024, 12, 30), "0800-1700"), - (datetime.date(2024, 12, 31), "0800-1700"), -] diff --git a/pythclient/market_schedule.py b/pythclient/market_schedule.py new file mode 100644 index 0000000..cda5a16 --- /dev/null +++ b/pythclient/market_schedule.py @@ -0,0 +1,209 @@ +from typing import List, Dict, Optional, Tuple +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError + +# A time is a string of the form HHMM and 2400 is used to represent midnight +Time = str + +# A time range is a tuple of two times, representing the start and end of a trading session. This +# range is *inclusive of the start time and exclusive of the end time*. +TimeRange = Tuple[Time, Time] + +# A daily schedule is a list of time ranges, representing the trading sessions for a given day +DailySchedule = List[TimeRange] + +class MarketSchedule: + def __init__(self, schedule_str: str): + """ + Parse a schedule string in Pyth format: "Timezone;WeeklySchedule;Holidays" + """ + + parts = schedule_str.split(';') + if len(parts) < 2: + raise ValueError("Schedule must contain at least timezone and weekly schedule") + + self.timezone = self._validate_timezone(parts[0]) + # Parse and validate weekly schedule - now returns parsed time ranges + self.weekly_schedule = self._parse_weekly_schedule(parts[1]) + self.holiday_schedule = self._parse_holidays(parts[2] if len(parts) > 2 else "") + + def _validate_timezone(self, timezone_str: str) -> ZoneInfo: + try: + return ZoneInfo(timezone_str) + except ZoneInfoNotFoundError: + raise ValueError(f"Invalid timezone: {timezone_str}") + + def _parse_weekly_schedule(self, schedule: str) -> List[DailySchedule]: + """Parse the weekly schedule (Mon-Sun) into daily schedules""" + days = schedule.split(',') + if len(days) != 7: + raise ValueError("Weekly schedule must contain exactly 7 days") + + weekly_schedule = [] + for day_schedule in days: + try: + weekly_schedule.append(self._parse_daily_schedule(day_schedule)) + except ValueError as e: + raise ValueError(f"Invalid schedule format: {day_schedule}") from e + + return weekly_schedule + + def _parse_holidays(self, holidays: str) -> Dict[str, DailySchedule]: + """Parse holiday overrides in format MMDD/Schedule""" + if not holidays: + return {} + + holiday_dict = {} + for holiday in holidays.split(','): + if not holiday: + continue + date_str, schedule = holiday.split('/') + holiday_dict[date_str] = self._parse_daily_schedule(schedule) + return holiday_dict + + def _parse_daily_schedule(self, schedule: str) -> DailySchedule: + """Parse time ranges in format HHMM-HHMM or HHMM-HHMM&HHMM-HHMM""" + if schedule == 'O': + return [('0000', '2400')] + elif schedule == 'C': + return [] + + ranges = [] + for time_range in schedule.split('&'): + start, end = time_range.split('-') + + + # Validate time format (HHMM) + if not (len(start) == 4 and len(end) == 4 and + start.isdigit() and end.isdigit() and + 0 <= int(start[:2]) <= 23 and 0 <= int(start[2:]) <= 59 and + ((0 <= int(end[:2]) <= 23 and 0 <= int(end[2:]) <= 59) or end == "2400")): + raise ValueError(f"Invalid time format in schedule: {start}-{end}") + + ranges.append((start, end)) + + # Sort ranges by start time + ranges.sort(key=lambda x: x[0]) + + return ranges + + def _get_time_ranges_for_date(self, dt: datetime) -> List[TimeRange]: + """Helper function to get time ranges for a specific date, checking holiday schedule first""" + date_str = dt.strftime("%m%d") + if date_str in self.holiday_schedule: + return self.holiday_schedule[date_str] + return self.weekly_schedule[dt.weekday()] + + def is_market_open(self, dt: datetime) -> bool: + """Check if the market is open at the given datetime""" + # Convert to market timezone + local_dt = dt.astimezone(self.timezone) + time_ranges = self._get_time_ranges_for_date(local_dt) + + if not time_ranges: + return False + + # Check current time against ranges + current_time = local_dt.strftime("%H%M") + return any(start <= current_time < end for start, end in time_ranges) + + def get_next_market_open(self, dt: datetime) -> Optional[datetime]: + """Get the next market open datetime after the given datetime. If the market + is open at the given datetime (even if just opens at the given time), + returns the next open datetime. + + If the market is always open, returns None. The returned datetime is in + the timezone of the input datetime.""" + input_tz = dt.tzinfo + current = dt.astimezone(self.timezone) + + # This flag is a invariant that indicates whether we're in the initial + # trading session of the given datetime as we move forward in time. + in_initial_trading_session = True + + # Look ahead up to 14 days + for _ in range(14): + time_ranges = self._get_time_ranges_for_date(current) + + current_time = current.strftime("%H%M") + + # Find the next open time after current_time + next_open = None + for start, end in time_ranges: + # If the end time is before the current time, skip + if end < current_time: + continue + + # If we're in the middle of a trading session, look for next session + # the complicated logic is to handle the distinction between + # a trading session that continues past midnight and one that doesn't + if start < current_time < end or (in_initial_trading_session and start <= current_time < end): + continue + + # Reaching here means we're no longer in a trading session + in_initial_trading_session = False + + # If this start time is after current time, this is the next open + if current_time < start: + next_open = start + break + + if next_open is not None: + # Found next opening time today + hour, minute = int(next_open[:2]), int(next_open[2:]) + result = current.replace(hour=hour, minute=minute, second=0, microsecond=0) + return result.astimezone(input_tz) # Convert back to input timezone + + # Move to next day at midnight + current = (current + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + + # There is a potential edge case where the market immediately opens at midnight (when rolling over to the next day) + # In this case, the new current time is the open time. + if self.is_market_open(current) and not self.is_market_open(current - timedelta(minutes=1)): + return current.astimezone(input_tz) + + return None + + def get_next_market_close(self, dt: datetime) -> Optional[datetime]: + """Get the next market close datetime after the given datetime. If the + market just closes at the given datetime, returns the next close datetime. + + If the market is always open, returns None. The returned datetime is in + the timezone of the input datetime.""" + input_tz = dt.tzinfo + current = dt.astimezone(self.timezone) + + # Look ahead up to 14 days + for _ in range(14): + time_ranges = self._get_time_ranges_for_date(current) + + current_time = current.strftime("%H%M") + + # Find the next close time after current_time + next_close = None + for _, end in time_ranges: + # If the end time is before (or equal to) the current time, skip + if end <= current_time: + continue + + # If we're in a trading session or a new one starts and the end time + # doesn't roll over to the next day, this is the next close + if current_time < end and end < "2400": + next_close = end + break + + if next_close is not None: + # Found next closing time + hour, minute = int(next_close[:2]), int(next_close[2:]) + result = current.replace(hour=hour, minute=minute, second=0, microsecond=0) + return result.astimezone(input_tz) # Convert back to input timezone + + # Move to next day at midnight + current = (current + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + + # There is a potential edge case where the market is not open at midnight (when rolling over to the next day) + # In this case, the new current time is the close time. + if not self.is_market_open(current) and self.is_market_open(current - timedelta(minutes=1)): + return current.astimezone(input_tz) + + return None diff --git a/pythclient/pythaccounts.py b/pythclient/pythaccounts.py index a035c43..0577f7e 100644 --- a/pythclient/pythaccounts.py +++ b/pythclient/pythaccounts.py @@ -7,6 +7,8 @@ from loguru import logger +from pythclient.market_schedule import MarketSchedule + from . import exceptions from .solana import SolanaPublicKey, SolanaPublicKeyOrStr, SolanaClient, SolanaAccount @@ -229,6 +231,13 @@ def symbol(self) -> str: Gets this account's symbol, or 'Unknown' if there is no 'symbol' attribute. """ return self.attrs.get("symbol", "Unknown") + + @property + def schedule(self) -> MarketSchedule: + """ + Gets the market schedule for this product. If the schedule is not set, returns an always open schedule. + """ + return MarketSchedule(self.attrs.get("schedule", "America/New_York;O,O,O,O,O,O,O;")) async def get_prices(self) -> Dict[PythPriceType, PythPriceAccount]: """ diff --git a/setup.py b/setup.py index 5c0e833..51f0adf 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pythclient', - version='0.1.24', + version='0.2.0', packages=['pythclient'], author='Pyth Developers', author_email='contact@pyth.network', diff --git a/tests/test_calendar.py b/tests/test_calendar.py deleted file mode 100644 index 13789e8..0000000 --- a/tests/test_calendar.py +++ /dev/null @@ -1,489 +0,0 @@ -import datetime -from zoneinfo import ZoneInfo - -from pythclient.calendar import (get_next_market_close, get_next_market_open, - is_market_open) -from pythclient.calendar_full_intervals import (EQUITY_2024_INTERVALS, - FX_2024_INTERVALS, - METAL_2024_INTERVALS, - RATES_2024_INTERVALS) - -NY_TZ = ZoneInfo("America/New_York") -UTC_TZ = ZoneInfo("UTC") - -# Define constants for equity market -EQUITY_OPEN_WED_2023_6_21_12 = datetime.datetime(2023, 6, 21, 12, 0, 0, tzinfo=NY_TZ) -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_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 -FX_METAL_OPEN_WED_2023_6_21_21 = datetime.datetime(2023, 6, 21, 21, 0, 0, tzinfo=NY_TZ) -FX_METAL_OPEN_WED_2023_6_21_23 = datetime.datetime(2023, 6, 21, 23, 0, 0, tzinfo=NY_TZ) -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) -FX_METAL_HOLIDAY_SUN_2023_12_24_17 = datetime.datetime(2023, 12, 24, 17, 0, 0, tzinfo=NY_TZ) -FX_METAL_HOLIDAY_SUN_2023_12_31_17 = datetime.datetime(2023, 12, 31, 17, 0, 0, tzinfo=NY_TZ) - -METAL_EARLY_HOLIDAY_MON_2024_1_15_13 = datetime.datetime(2024, 1, 15, 13, 0, 0, tzinfo=NY_TZ) -METAL_EARLY_HOLIDAY_MON_2024_1_15_17 = datetime.datetime(2024, 1, 15, 17, 0, 0, tzinfo=NY_TZ) -METAL_EARLY_HOLIDAY_MON_2024_1_15_18 = datetime.datetime(2024, 1, 15, 18, 0, 0, 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) - - -def format_datetime_to_unix_timestamp(dt: datetime.datetime): - # Convert the datetime object to a Unix timestamp in UTC - timestamp = dt.astimezone(UTC_TZ).timestamp() - unix_timestamp_utc = int(timestamp) - return unix_timestamp_utc - - -def test_is_market_open(): - # equity - # weekday, within equity market hours - assert is_market_open("equity", EQUITY_OPEN_WED_2023_6_21_12) == True - - # weekday, out of equity market hours - assert is_market_open("equity", EQUITY_CLOSE_WED_2023_6_21_17) == False - - # weekend, out of equity market hours - assert is_market_open("equity", EQUITY_CLOSE_SAT_2023_6_10_17) == False - - # weekday, NYSE holiday - 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_11) == True - assert is_market_open("equity", EQUITY_EARLY_CLOSE_CLOSE_FRI_2023_11_24_14) == False - - # fx & metal - # weekday, within fx & metal market hours - assert is_market_open("fx", FX_METAL_OPEN_WED_2023_6_21_21) == True - assert is_market_open("metal", FX_METAL_OPEN_WED_2023_6_21_21) == True - - # weekday, out of fx & metal market hours - assert is_market_open("fx", FX_METAL_CLOSE_SUN_2023_6_18_16) == False - assert is_market_open("metal", FX_METAL_CLOSE_SUN_2023_6_18_16) == False - - # fx & metal holiday - 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 - - # metal early holiday - assert is_market_open("metal", METAL_EARLY_HOLIDAY_MON_2024_1_15_13) == True - assert is_market_open("metal", METAL_EARLY_HOLIDAY_MON_2024_1_15_17) == False - assert is_market_open("metal", METAL_EARLY_HOLIDAY_MON_2024_1_15_18) == True - - - # fx & metal out of market hours on Sunday Dec 24 2023 after 10pm UTC - assert is_market_open("fx", FX_METAL_HOLIDAY_SUN_2023_12_24_17) == False - - # fx & metal out of market hours on Sunday Dec 31 2023 after 10pm UTC - assert is_market_open("fx", FX_METAL_HOLIDAY_SUN_2023_12_31_17) == 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 - - -def test_get_next_market_open(): - # equity within market hours - assert ( - get_next_market_open("equity", EQUITY_OPEN_WED_2023_6_21_12) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 22, 9, 30, 0, tzinfo=NY_TZ)) - ) - - # equity out of market hours - assert ( - get_next_market_open("equity", EQUITY_CLOSE_WED_2023_6_21_17) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 22, 9, 30, 0, tzinfo=NY_TZ)) - ) - - # equity weekend - assert ( - get_next_market_open("equity", EQUITY_CLOSE_SAT_2023_6_10_17) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 12, 9, 30, 0, tzinfo=NY_TZ)) - ) - - # equity holiday - assert ( - get_next_market_open("equity", EQUITY_HOLIDAY_MON_2023_6_19) - == 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_11) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 27, 9, 30, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_open("equity", EQUITY_EARLY_CLOSE_CLOSE_FRI_2023_11_24_14) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 27, 9, 30, 0, tzinfo=NY_TZ)) - ) - - # fx & metal within market hours (before 10pm UTC) - assert ( - get_next_market_open("fx", FX_METAL_OPEN_WED_2023_6_21_21) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 25, 17, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_open("metal", FX_METAL_OPEN_WED_2023_6_21_21) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 25, 17, 0, 0, tzinfo=NY_TZ)) - ) - # fx & metal within market hours (after 10pm UTC) - assert ( - get_next_market_open("fx", FX_METAL_OPEN_WED_2023_6_21_23) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 25, 17, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_open("metal", FX_METAL_OPEN_WED_2023_6_21_23) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 25, 17, 0, 0, tzinfo=NY_TZ)) - ) - - # fx & metal out of market hours - assert ( - get_next_market_open("fx", FX_METAL_CLOSE_SUN_2023_6_18_16) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 18, 17, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_open("metal", FX_METAL_CLOSE_SUN_2023_6_18_16) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 18, 17, 0, 0, tzinfo=NY_TZ)) - ) - - # fx & metal out of market hours on Sunday Dec 24 2023 after 5pm ET - assert ( - get_next_market_open("fx", FX_METAL_HOLIDAY_SUN_2023_12_24_17) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 12, 25, 17, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_open("metal", FX_METAL_HOLIDAY_SUN_2023_12_24_17) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 12, 25, 17, 0, 0, tzinfo=NY_TZ)) - ) - - # fx & metal out of market hours on holiday Dec 25 2023 before 5pm ET - assert ( - get_next_market_open("fx", datetime.datetime(2023, 12, 25, 8, 15, 0, tzinfo=NY_TZ)) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 12, 25, 17, 0, 0, tzinfo=NY_TZ)) - ) - - # fx & metal holiday - assert ( - get_next_market_open("fx", FX_METAL_HOLIDAY_SUN_2023_1_1) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 1, 1, 17, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_open("metal", FX_METAL_HOLIDAY_SUN_2023_1_1) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 1, 1, 17, 0, 0, tzinfo=NY_TZ)) - ) - - # metal early holiday - assert ( - get_next_market_open("metal", METAL_EARLY_HOLIDAY_MON_2024_1_15_13) - == format_datetime_to_unix_timestamp(datetime.datetime(2024, 1, 15, 18, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_open("metal", METAL_EARLY_HOLIDAY_MON_2024_1_15_17) - == format_datetime_to_unix_timestamp(datetime.datetime(2024, 1, 15, 18, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_open("metal", METAL_EARLY_HOLIDAY_MON_2024_1_15_18) - == format_datetime_to_unix_timestamp(datetime.datetime(2024, 1, 21, 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 - - -def test_get_next_market_close(): - # equity within market hours - assert ( - get_next_market_close("equity", EQUITY_OPEN_WED_2023_6_21_12) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 21, 16, 0, 0, tzinfo=NY_TZ)) - ) - - # equity out of market hours - assert ( - get_next_market_close("equity", EQUITY_CLOSE_WED_2023_6_21_17) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 22, 16, 0, 0, tzinfo=NY_TZ)) - ) - - # equity weekend - assert ( - get_next_market_close("equity", EQUITY_CLOSE_SAT_2023_6_10_17) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 12, 16, 0, 0, tzinfo=NY_TZ)) - ) - - # equity holiday - assert ( - get_next_market_close("equity", EQUITY_HOLIDAY_MON_2023_6_19) - == 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_11) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 24, 13, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_close("equity", EQUITY_EARLY_CLOSE_CLOSE_FRI_2023_11_24_14) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 27, 16, 0, 0, tzinfo=NY_TZ)) - ) - - # fx & metal within market hours (before 10pm UTC) - assert ( - get_next_market_close("fx", FX_METAL_OPEN_WED_2023_6_21_21) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 23, 17, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_close("metal", FX_METAL_OPEN_WED_2023_6_21_21) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 23, 17, 0, 0, tzinfo=NY_TZ)) - ) - - # fx & metal within market hours (after 10pm UTC) - assert ( - get_next_market_close("fx", FX_METAL_OPEN_WED_2023_6_21_23) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 23, 17, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_close("metal", FX_METAL_OPEN_WED_2023_6_21_23) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 23, 17, 0, 0, tzinfo=NY_TZ)) - ) - - # fx & metal within market hours on a friday (before 10pm UTC) - assert ( - get_next_market_close("fx", datetime.datetime(2023, 11, 10, 7, 0, 0, tzinfo=NY_TZ)) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 11, 10, 17, 0, 0, tzinfo=NY_TZ)) - ) - - # fx & metal out of market hours - assert ( - get_next_market_close("fx", FX_METAL_CLOSE_SUN_2023_6_18_16) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 23, 17, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_close("metal", FX_METAL_CLOSE_SUN_2023_6_18_16) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 6, 23, 17, 0, 0, tzinfo=NY_TZ)) - ) - - # fx & metal holiday - assert ( - get_next_market_close("fx", FX_METAL_HOLIDAY_SUN_2023_1_1) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 1, 6, 17, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_close("metal", FX_METAL_HOLIDAY_SUN_2023_1_1) - == format_datetime_to_unix_timestamp(datetime.datetime(2023, 1, 6, 17, 0, 0, tzinfo=NY_TZ)) - ) - - # metal early holiday - assert ( - get_next_market_close("metal", METAL_EARLY_HOLIDAY_MON_2024_1_15_13) - == format_datetime_to_unix_timestamp(datetime.datetime(2024, 1, 15, 14, 30, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_close("metal", METAL_EARLY_HOLIDAY_MON_2024_1_15_17) - == format_datetime_to_unix_timestamp(datetime.datetime(2024, 1, 19, 17, 0, 0, tzinfo=NY_TZ)) - ) - assert ( - get_next_market_close("metal", METAL_EARLY_HOLIDAY_MON_2024_1_15_18) - == format_datetime_to_unix_timestamp(datetime.datetime(2024, 1, 19, 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 - - -def test_is_market_open_full(): - start_date = datetime.datetime(2024, 1, 1, tzinfo=NY_TZ) - end_date = datetime.datetime(2025, 1, 1, tzinfo=NY_TZ) - asset_types = ["equity", "fx", "metal", "rates", "crypto"] - - all_intervals = { - "equity": {}, - "fx": {}, - "metal": {}, - "rates": {}, - } - - data_sources = { - "equity": EQUITY_2024_INTERVALS, - "fx": FX_2024_INTERVALS, - "metal": METAL_2024_INTERVALS, - "rates": RATES_2024_INTERVALS, - } - - for asset_type, data in data_sources.items(): - for date, interval in data: - if date not in all_intervals[asset_type]: - all_intervals[asset_type][date] = [] - if interval != None: - all_intervals[asset_type][date].append(interval) - - current_date = start_date - while current_date < end_date: - for at in asset_types: - if at == "crypto": - continue - # Get the interval for the date - intervals = all_intervals[at].get(current_date.date()) - - if not intervals: - should_be_open = False - else: - should_be_open = is_time_in_intervals(current_date.time(), intervals) - pass - - # Check if the market is open - is_open = is_market_open(at, current_date) - - # Assert that the market is open if and only if it should be open - assert ( - is_open == should_be_open - ), f"Failed for asset type: {at}, date: {current_date}" - - # Add one minute to the current date - current_date += datetime.timedelta(minutes=1) - - -def is_time_in_intervals(current_time, intervals): - for interval in intervals: - start_time, end_time = [ - datetime.datetime.strptime(t, "%H%M").time() for t in interval.split("-") - ] - if start_time < end_time: - if start_time <= current_time < end_time: - return True - else: # Over midnight - if start_time <= current_time or current_time < end_time: - return True - return False diff --git a/tests/test_market_schedule.py b/tests/test_market_schedule.py new file mode 100644 index 0000000..888b481 --- /dev/null +++ b/tests/test_market_schedule.py @@ -0,0 +1,152 @@ +import pytest +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo +from pythclient.market_schedule import MarketSchedule + +# This fixtures are based on the schedule from the metadata of the assets taken at 2024-12-20 +FIXTURES = { + "amazonusd": "America/New_York;0930-1600,0930-1600,0930-1600,0930-1600,0930-1600,C,C;1224/0930-1300,1225/C,0101/C,0120/C,0217/C,0418/C,0526/C,0619/C", + "brent1musd": "America/New_York;0000-1800&2000-2400,0000-1800&2000-2400,0000-1800&2000-2400,0000-1800&2000-2400,0000-1800,C,1800-2400;1224/0000-1400&2000-2400,1225/C,0101/C,0418/C", + "btcusd": "America/New_York;O,O,O,O,O,O,O;", + "eurusd": "America/New_York;O,O,O,O,0000-1700,C,1700-2400;1224/0000-1700,1225/1700-2400,1231/0000-1700,0101/1700-2400", +} + +def test_market_open(): + # Test Amazon trading hours (regular NYSE hours) + schedule = MarketSchedule(FIXTURES["amazonusd"]) + + # Close at 9:29 ET + assert not schedule.is_market_open(datetime(2024, 3, 1, 9, 29, tzinfo=ZoneInfo("UTC"))) # 9:29 ET + # Open at 9:30 ET + assert schedule.is_market_open(datetime(2024, 3, 1, 14, 30, tzinfo=ZoneInfo("UTC"))) # 9:30 ET + + # Regular trading day + assert schedule.is_market_open(datetime(2024, 3, 1, 19, 30, tzinfo=ZoneInfo("UTC"))) # 14:30 ET + + # Open at 15:59 ET + assert schedule.is_market_open(datetime(2024, 3, 1, 20, 59, tzinfo=ZoneInfo("UTC"))) # 15:59 ET + # Close at 16:00 ET + assert not schedule.is_market_open(datetime(2024, 3, 1, 21, 0, tzinfo=ZoneInfo("UTC"))) # 16:00 ET + + # Weekend + assert not schedule.is_market_open(datetime(2024, 3, 2, 14, 30, tzinfo=ZoneInfo("UTC"))) # Saturday 9:30 ET + + # Holiday (Christmas) - market is closed + assert not schedule.is_market_open(datetime(2024, 12, 25, 14, 30, tzinfo=ZoneInfo("UTC"))) # Christmas 25/12/2024 9:30 ET + + # Holiday (Christmas Eve) - market has early close + assert not schedule.is_market_open(datetime(2024, 12, 24, 14, 29, tzinfo=ZoneInfo("UTC"))) # Christmas Eve 24/12/2024 9:29 ET + assert schedule.is_market_open(datetime(2024, 12, 24, 14, 30, tzinfo=ZoneInfo("UTC"))) # Christmas Eve 24/12/2024 9:30 ET + assert schedule.is_market_open(datetime(2024, 12, 24, 17, 59, tzinfo=ZoneInfo("UTC"))) # Christmas Eve 24/12/2024 12:59 ET + assert not schedule.is_market_open(datetime(2024, 12, 24, 18, 0, tzinfo=ZoneInfo("UTC"))) # Christmas Eve 24/12/2024 13:00 ET + +def test_next_market_open(): + schedule = MarketSchedule(FIXTURES["amazonusd"]) + + # Test next open from weekend + dt = datetime(2024, 3, 2, 12, 0, tzinfo=ZoneInfo("UTC")) # Saturday + next_open = schedule.get_next_market_open(dt) + assert next_open.strftime("%Y-%m-%d %H:%M") == "2024-03-04 14:30" # Monday 9:30 ET + + # Test next open from current open trading session (right at the start of the session) + dt = datetime(2024, 3, 1, 14, 30, tzinfo=ZoneInfo("UTC")) # Friday 9:30 ET + next_open = schedule.get_next_market_open(dt) + assert next_open.strftime("%Y-%m-%d %H:%M") == "2024-03-04 14:30" # Monday 9:30 ET + +def test_next_market_close(): + schedule = MarketSchedule(FIXTURES["amazonusd"]) + + # Test next close during trading hours + dt = datetime(2024, 3, 1, 14, 30, tzinfo=ZoneInfo("UTC")) # Friday 9:30 ET + next_close = schedule.get_next_market_close(dt) + assert next_close == datetime(2024, 3, 1, 21, 0, tzinfo=ZoneInfo("UTC")) + + # Test next close from the end of the trading session + dt = datetime(2024, 3, 1, 21, 0, tzinfo=ZoneInfo("UTC")) # Friday 16:00 ET + next_close = schedule.get_next_market_close(dt) + assert next_close == datetime(2024, 3, 4, 21, 0, tzinfo=ZoneInfo("UTC")) + + # Test next close from weekend + dt = datetime(2024, 3, 2, 12, 0, tzinfo=ZoneInfo("UTC")) # Saturday 8:00 ET + next_close = schedule.get_next_market_close(dt) + assert next_close == datetime(2024, 3, 4, 21, 0, tzinfo=ZoneInfo("UTC")) + +def test_complex_schedule_brent1musd(): + """Test Brent oil futures with multiple sessions per day""" + + schedule = MarketSchedule(FIXTURES["brent1musd"]) + + # Test during first session, 4 Dec 2024 (Wednesday) + assert schedule.is_market_open(datetime(2024, 12, 4, 0, 0, tzinfo=ZoneInfo("America/New_York"))) + assert schedule.is_market_open(datetime(2024, 12, 4, 12, 0, tzinfo=ZoneInfo("America/New_York"))) + + # Test during gap between sessions + assert not schedule.is_market_open(datetime(2024, 12, 4, 18, 0, tzinfo=ZoneInfo("America/New_York"))) + assert not schedule.is_market_open(datetime(2024, 12, 4, 19, 0, tzinfo=ZoneInfo("America/New_York"))) + + # Test during second session + assert schedule.is_market_open(datetime(2024, 12, 4, 20, 0, tzinfo=ZoneInfo("America/New_York"))) + assert schedule.is_market_open(datetime(2024, 12, 4, 23, 59, tzinfo=ZoneInfo("America/New_York"))) + + # Test next market close + next_close = schedule.get_next_market_close(datetime(2024, 12, 4, 23, 59, tzinfo=ZoneInfo("America/New_York"))) + assert next_close == datetime(2024, 12, 5, 18, 0, tzinfo=ZoneInfo("America/New_York")) + + # Test next market open + next_open = schedule.get_next_market_open(datetime(2024, 12, 4, 23, 59, tzinfo=ZoneInfo("America/New_York"))) + assert next_open == datetime(2024, 12, 5, 20, 0, tzinfo=ZoneInfo("America/New_York")) + +def test_always_open_schedule(): + """Test a schedule that is always open""" + + schedule = MarketSchedule(FIXTURES["btcusd"]) + assert schedule.is_market_open(datetime(2024, 12, 3, 23, 59, tzinfo=ZoneInfo("America/New_York"))) + + # Make sure next market open and next market close are None + assert schedule.get_next_market_open(datetime(2024, 12, 3, 23, 59, tzinfo=ZoneInfo("America/New_York"))) is None + assert schedule.get_next_market_close(datetime(2024, 12, 3, 23, 59, tzinfo=ZoneInfo("America/New_York"))) is None + +def test_invalid_schedules(): + # Test invalid timezone + with pytest.raises(ValueError): + MarketSchedule("Invalid/Timezone;0930-1600,0930-1600,0930-1600,0930-1600,0930-1600,C,C") + + # Test invalid number of days + with pytest.raises(ValueError): + MarketSchedule("America/New_York;0930-1600,0930-1600,0930-1600,0930-1600,0930-1600,C") + + # Test invalid time format + with pytest.raises(ValueError): + MarketSchedule("America/New_York;1600-2500,0930-1600,0930-1600,0930-1600,0930-1600,C,C") + + # Test invalid schedule format + with pytest.raises(ValueError): + MarketSchedule("America/New_York;0930-1600-1700,0930-1600,0930-1600,0930-1600,0930-1600,C,C") + + # Test invalid holiday schedule + with pytest.raises(ValueError): + MarketSchedule("America/New_York;0930-1600,0930-1600,0930-1600,0930-1600,0930-1600,C,C;1224/0930-2500") + +@pytest.mark.parametrize("asset", ["amazonusd", "brent1musd", "eurusd"]) +def test_walk_backwards_through_schedule(asset): + """Test walking backwards through the schedule to test the next market open and next market close""" + schedule = MarketSchedule(FIXTURES[asset]) + + current_time = datetime(2025, 6, 6, 0, 0, tzinfo=ZoneInfo("America/New_York")) + expected_next_market_open = None + expected_next_market_close = None + + while datetime(2024, 6, 6, 0, 0, tzinfo=ZoneInfo("America/New_York")) < current_time: + # update the expected next market open and next market close + if not schedule.is_market_open(current_time) and schedule.is_market_open(current_time + timedelta(minutes=1)): + expected_next_market_open = current_time + timedelta(minutes=1) + if schedule.is_market_open(current_time) and not schedule.is_market_open(current_time + timedelta(minutes=1)): + expected_next_market_close = current_time + timedelta(minutes=1) + + next_market_open = schedule.get_next_market_open(current_time) + next_market_close = schedule.get_next_market_close(current_time) + + assert not expected_next_market_open or next_market_open == expected_next_market_open + assert not expected_next_market_close or next_market_close == expected_next_market_close + + current_time -= timedelta(minutes=1) From 34040dc5b50defd87b7495779e75680a6f93305e Mon Sep 17 00:00:00 2001 From: Ciaran Moran Date: Tue, 11 Feb 2025 17:35:47 +0000 Subject: [PATCH 2/6] Bump Ubuntu release GitHub deprecating Ubuntu 20.04: https://github.com/actions/runner-images/issues/11101 --- .github/workflows/pypi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index a45fcfe..425e984 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -7,7 +7,7 @@ on: jobs: deploy: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 From 65c2d925978093325db68ded9979789632072347 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Wed, 12 Feb 2025 11:59:27 -0800 Subject: [PATCH 3/6] feat: add max latency field (#59) --- pythclient/pythaccounts.py | 21 ++- setup.py | 2 +- tests/test_price_account.py | 327 +++++++++++++++++++++++++++--------- tests/test_pyth_client.py | 6 +- 4 files changed, 269 insertions(+), 87 deletions(-) diff --git a/pythclient/pythaccounts.py b/pythclient/pythaccounts.py index 0577f7e..6013b25 100644 --- a/pythclient/pythaccounts.py +++ b/pythclient/pythaccounts.py @@ -17,9 +17,9 @@ _VERSION_1 = 1 _VERSION_2 = 2 _SUPPORTED_VERSIONS = set((_VERSION_1, _VERSION_2)) -_ACCOUNT_HEADER_BYTES = 16 # magic + version + type + size, u32 * 4 +ACCOUNT_HEADER_BYTES = 16 # magic + version + type + size, u32 * 4 _NULL_KEY_BYTES = b'\x00' * SolanaPublicKey.LENGTH -MAX_SLOT_DIFFERENCE = 25 +DEFAULT_MAX_LATENCY = 25 class PythAccountType(Enum): @@ -81,7 +81,7 @@ def _read_attribute_string(buffer: bytes, offset: int) -> Tuple[Optional[str], i def _parse_header(buffer: bytes, offset: int = 0, *, key: SolanaPublicKeyOrStr) -> Tuple[PythAccountType, int, int]: - if len(buffer) - offset < _ACCOUNT_HEADER_BYTES: + if len(buffer) - offset < ACCOUNT_HEADER_BYTES: raise ValueError("Pyth account data too short") # Pyth magic (u32) == MAGIC @@ -141,7 +141,7 @@ def update_with_rpc_response(self, slot: int, value: Dict[str, Any]) -> None: f"wrong Pyth account type {type_} for {type(self)}") try: - self.update_from(data[:size], version=version, offset=_ACCOUNT_HEADER_BYTES) + self.update_from(data[:size], version=version, offset=ACCOUNT_HEADER_BYTES) except Exception as e: logger.exception("error while parsing account", exception=e) @@ -482,6 +482,7 @@ class PythPriceAccount(PythAccount): aggregate price is composed of slot (int): the slot time when this account was last fetched product (Optional[PythProductAccount]): the product this price is for, if loaded + max_latency (int): the maximum allowed slot difference for this feed """ def __init__(self, key: SolanaPublicKey, solana: SolanaClient, *, product: Optional[PythProductAccount] = None) -> None: @@ -503,6 +504,7 @@ def __init__(self, key: SolanaPublicKey, solana: SolanaClient, *, product: Optio self.prev_price: float = field(init=False) self.prev_conf: float = field(init=False) self.prev_timestamp: int = 0 # unix timestamp in seconds + self.max_latency: int = 0 # maximum allowed slot difference for this feed @property def aggregate_price(self) -> Optional[float]: @@ -537,7 +539,7 @@ def get_aggregate_price_status_with_slot(self, slot: int) -> Optional[PythPriceS You might consider using this function with the latest solana slot to make sure the price has not gone stale. """ if self.aggregate_price_info.price_status == PythPriceStatus.TRADING and \ - slot - self.aggregate_price_info.pub_slot > MAX_SLOT_DIFFERENCE: + slot - self.aggregate_price_info.pub_slot > self.max_latency: return PythPriceStatus.UNKNOWN return self.aggregate_price_info.price_status @@ -571,9 +573,12 @@ def update_from(self, buffer: bytes, *, version: int, offset: int = 0) -> None: derivations = list(struct.unpack_from("<6q", buffer, offset)) self.derivations = dict((type_, derivations[type_.value - 1]) for type_ in [EmaType.EMA_CONFIDENCE_VALUE, EmaType.EMA_PRICE_VALUE]) offset += 48 # struct.calcsize("6q") - # drv[2-4]_ fields are currently unused timestamp, min_publishers = struct.unpack_from(" None: self.prev_price = prev_price self.prev_conf = prev_conf self.prev_timestamp = prev_timestamp + # a max latency of 0 is the default max latency + self.max_latency = max_latency if max_latency != 0 else DEFAULT_MAX_LATENCY def __str__(self) -> str: if self.product: diff --git a/setup.py b/setup.py index 51f0adf..339a2f0 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pythclient', - version='0.2.0', + version='0.2.1', packages=['pythclient'], author='Pyth Developers', author_email='contact@pyth.network', diff --git a/tests/test_price_account.py b/tests/test_price_account.py index 2ac3254..62e7c02 100644 --- a/tests/test_price_account.py +++ b/tests/test_price_account.py @@ -3,7 +3,8 @@ from dataclasses import asdict from pythclient.pythaccounts import ( - MAX_SLOT_DIFFERENCE, + ACCOUNT_HEADER_BYTES, + DEFAULT_MAX_LATENCY, PythPriceAccount, PythPriceType, PythPriceStatus, @@ -12,49 +13,222 @@ from pythclient.solana import SolanaPublicKey, SolanaClient -# Yes, this sucks, but it is actually a monster datastructure (2K) +# Yes, this sucks, but it is actually a monster datastructure +# Equity.US.AAPL/USD symbol @pytest.fixture def price_account_bytes(): return base64.b64decode(( - b'AQAAAPj///8TAAAAEAAAANupUgYAAAAA2qlSBgAAAAB4XGx3EAAAAJ86jskAAAAA3CH+HAEA' - b'AAD6ORUDAAAAABzzZ5MAAAAA3CH+HAEAAAABAAAAAAAAAAAAAAAAAAAASNYDPXM+J5UMLgNR' - b'4lBUkc2RVIJPcW2VE1FMdLn5j1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANmp' - b'UgYAAAAAIB/LdhAAAADEKi0CAAAAAAAAAAAAAAAAIB/LdhAAAADk7y4CAAAAAAEAAAAAAAAA' - b'26lSBgAAAAD3Zn27jXzTFyGJQUojwmtSHYwZxkUEVVXj4o6CVo5leGBYnXYQAAAAYBBIAQAA' - b'AAABAAAAAAAAANipUgYAAAAAYFiddhAAAABgEEgBAAAAAAEAAAAAAAAA2alSBgAAAAAWD7rB' - b'Ovfd2AXTFwo94Ma9lxJqHgLA0lnQqG74IdblxyAfy3YQAAAACyXuAAAAAAABAAAAAAAAANip' - b'UgYAAAAAIB/LdhAAAAALJe4AAAAAAAEAAAAAAAAA2alSBgAAAAAF0gZPMxz/3cq+lvo2VSTd' - b'ZPSzhuiFo00VLL6uBCzq9cgcPHEQAAAAVAF9BAAAAAABAAAAAAAAANmpUgYAAAAAyBw8cRAA' - b'AABUAX0EAAAAAAEAAAAAAAAA2qlSBgAAAADiuY8mkITUiAURyBFdzvBPU8fiB5kuA//RJt+U' - b'TeTbBEB4h9UNAAAAQFSJAAAAAAABAAAAAAAAANfOOgYAAAAAQHiH1Q0AAABAVIkAAAAAAAEA' - b'AAAAAAAA1846BgAAAAAa5QKj6UK4sRzDdElrTZxcOfgMXawfRZ81og7BuHMIndC7Q3UQAAAA' - b'cNddAAAAAAABAAAAAAAAANipUgYAAAAA0LtDdRAAAABw110AAAAAAAEAAAAAAAAA2alSBgAA' - b'AAANw7zqkVVpdgiwXwSnCtCaQVFyqpu190CHOsB4KysaRYA0/X4QAAAA6PWmKgAAAAABAAAA' - b'AAAAANipUgYAAAAAgDT9fhAAAADo9aYqAAAAAAEAAAAAAAAA2alSBgAAAAAH8ss5/bAp3FF4' - b'TSjvF5Edl8GmnIVyOhtiVbNCU0OtdaDilHgQAAAAQHh9AQAAAAABAAAAAAAAANmpUgYAAAAA' - b'oOKUeBAAAABAeH0BAAAAAAEAAAAAAAAA2qlSBgAAAACfPqV71Am6AMQNkq5XE0HCfjwvft+s' - b'4cJKUbGhXDGytwB+w3YQAAAAAJDQAwAAAAABAAAAAAAAANapUgYAAAAAAH7DdhAAAAAAkNAD' - b'AAAAAAEAAAAAAAAA1qlSBgAAAABDgo+jYZ2mvK7WiRfeHXzOkhfexyuuEjBj/3vn3S+WPp+1' - b'Y3cQAAAAIND8AwAAAAABAAAAAAAAANipUgYAAAAAn7VjdxAAAADQDPwDAAAAAAEAAAAAAAAA' - b'2alSBgAAAAAYg7EkbdpdBxc9vTjVZwAHFYQsH9DolucLCm3S5RpPl5CJPXoQAAAAoH4mAQAA' - b'AAABAAAAAAAAANSpUgYAAAAAkIk9ehAAAACgfiYBAAAAAAEAAAAAAAAA1KlSBgAAAABDt3hL' - b'b4VmyzKDZfvOC0BGFSO67OeFF7MVXDHaozgpj6CarHYQAAAA4MrUAgAAAAABAAAAAAAAANip' - b'UgYAAAAAoJqsdhAAAADgytQCAAAAAAEAAAAAAAAA2alSBgAAAAD1nd3vzBZrLYmko8zz/sS7' - b'S5ihUbTAN/9hXrt4QuM9dQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' - b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACp6tPj00vMhTS7LGUOsnqMjD8aItaIKEMoU4xC' - b'qOgjQwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' - b'AAAAAAAAAAAAAAAAAADQyjMc9dnucWvIxpCjAKuoQDs3FBy2OwJlwJjAxY5jrEDJD3cQAAAA' - b'iO90AgAAAAABAAAAAAAAANipUgYAAAAAQMkPdxAAAACI73QCAAAAAAEAAAAAAAAA2alSBgAA' - b'AABfyWT+IQLDTV2m/OVBHX+euZaDX9doeSPt8Afh6snTmyAfy3YQAAAA8MjSAAAAAAABAAAA' - b'AAAAANipUgYAAAAAIB/LdhAAAADwyNIAAAAAAAEAAAAAAAAA2alSBgAAAADshtw0V/2qFXMo' - b'0kCdNAHTz61GgIqwRBk8Hn7J+tXPYra163kQAAAAmhIJAwAAAAABAAAAAAAAANipUgYAAAAA' - b'trXreRAAAACaEgkDAAAAAAEAAAAAAAAA2alSBgAAAADYb4QN6+LtpmaFm/jCx0LD5ke+Thdt' - b'/FIl2ATx1J/iLU9xK3YQAAAAUQDeAQAAAAABAAAAAAAAANipUgYAAAAAT3ErdhAAAABRAN4B' - b'AAAAAAEAAAAAAAAA2alSBgAAAAD3oTB6i0MnB/D217PntQNBRQJinx7o+cT2tZFViRokLtlj' - b'UXkQAAAAhB8gAgAAAAABAAAAAAAAANipUgYAAAAA2WNReRAAAACEHyACAAAAAAEAAAAAAAAA' - b'2alSBgAAAAAQObH1+gS8Ag0HeG1UdQRs2fQLBW50YN8kJo4QAHwOnZ+DNHgQAAAAYe1IAgAA' - b'AAABAAAAAAAAANipUgYAAAAAn4M0eBAAAABh7UgCAAAAAAEAAAAAAAAA2alSBgAAAAA=' - )) + b'1MOyoQIAAAADAAAAIDEAAAEAAAD7////HQAAABsAAAD/rccLAAAAAP6txwsAAAAATKVnAQAAAACfFQcN' + b'AQAAAP0gJHIAAAAAczcAAAAAAAA8DgiiAAAAAP0gJHIAAAAA9fasZwAAAAADADIDPQEAACkunmg3xiSw' + b'fCBPOBN1xaL8HmQRPUjcgostWu2uVecsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+rccL' + b'AAAAAEp3aAEAAAAA+TcAAAAAAAD09qxnAAAAAM12aAEAAAAAdjgAAAAAAAABAAAAAAAAAP+txwsAAAAA' + b'BHkihWa8qHaHujLYgFXDIwjMb1piz6Z/GIGZQeOFsrJwcWgBAAAAALvEAAAAAAAAAQAAAAAAAAD8rccL' + b'AAAAAHBxaAEAAAAAu8QAAAAAAAABAAAAAAAAAPytxwsAAAAABw99DZApUxxfQ451HfqUNEecEJ+K3q4L' + b'ImQn81mfXBnOemgBAAAAANkUAQAAAAAAAQAAAAAAAADzrccLAAAAAM56aAEAAAAA2RQBAAAAAAABAAAA' + b'AAAAAPOtxwsAAAAAB/LLOf2wKdxReE0o7xeRHZfBppyFcjobYlWzQlNDrXVMd2gBAAAAAEhcAAAAAAAA' + b'AQAAAAAAAAD3rccLAAAAAEx3aAEAAAAASFwAAAAAAAABAAAAAAAAAPetxwsAAAAAC7W169huq2IOUmHg' + b'hY4UR1FAoCOpXo1cicOJgwqilmeucmgBAAAAAPQBAAAAAAAAAQAAAAAAAAD9rccLAAAAAK5yaAEAAAAA' + b'9AEAAAAAAAABAAAAAAAAAP2txwsAAAAAFzpASQCO7GVI83hRl/cs7iBjSV0Av1Bj68V8d837GUHzPVwB' + b'AAAAANBpwAAAAAAABAAAAAAAAAAd35ILAAAAAPM9XAEAAAAA0GnAAAAAAAAEAAAAAAAAAB3fkgsAAAAA' + b'GIOxJG3aXQcXPb041WcABxWELB/Q6JbnCwpt0uUaT5cJbWgBAAAAAFgHAwAAAAAAAQAAAAAAAAD8rccL' + b'AAAAAAltaAEAAAAAWAcDAAAAAAABAAAAAAAAAPytxwsAAAAAJh54j4GAISD3TwZWpS7jDYp6d0mcRf2n' + b'xlxmID4iZ25tgWgBAAAAAOWaAwAAAAAAAQAAAAAAAAD0rccLAAAAAG2BaAEAAAAA5ZoDAAAAAAABAAAA' + b'AAAAAPStxwsAAAAANIa+/riGb203XbXQ8h0HwnTKrhg+e3cLJXNgHPRZHSRRd2gBAAAAABwwAAAAAAAA' + b'AQAAAAAAAAD6rccLAAAAAFF3aAEAAAAAHDAAAAAAAAABAAAAAAAAAPqtxwsAAAAAQ4KPo2Gdpryu1okX' + b'3h18zpIX3scrrhIwY/97590vlj5nf2gBAAAAABh+AAAAAAAAAQAAAAAAAAD8rccLAAAAAGd/aAEAAAAA' + b'GH4AAAAAAAABAAAAAAAAAPytxwsAAAAATXYO0eWeK9NQsMMZj+HvA16XRS7UvMYr42xvExZSkdFcdWgB' + b'AAAAABmLAAAAAAAAAQAAAAAAAAD8rccLAAAAAFx1aAEAAAAAGYsAAAAAAAABAAAAAAAAAPytxwsAAAAA' + b'Tjqyi56CYuBQyurc9ATAapzuKuOgdEwh/hm0Mt5mkOtkdWgBAAAAANsQAQAAAAAAAQAAAAAAAAD+rccL' + b'AAAAAGR1aAEAAAAA2xABAAAAAAABAAAAAAAAAP6txwsAAAAATrAvjfOs/kT57qji7Ps3wu5XqD3//AFC' + b'0CdHbBz0M3QEnGgBAAAAAIpIAgAAAAAAAQAAAAAAAAD7rccLAAAAAAScaAEAAAAAikgCAAAAAAABAAAA' + b'AAAAAPutxwsAAAAAVBkdg3Zb8Ej6G4LYAW466xu/DHb3ezUTWu9Vo3T3/ms8e2gBAAAAAAc0AAAAAAAA' + b'AQAAAAAAAADyrccLAAAAADx7aAEAAAAABzQAAAAAAAABAAAAAAAAAPKtxwsAAAAAaj2lMUYld1Wxfrwl' + b'0Lo22hdeJPxpkprmafPfHmPVnUBafWgBAAAAAElcAAAAAAAAAQAAAAAAAADxrccLAAAAAFp9aAEAAAAA' + b'SVwAAAAAAAABAAAAAAAAAPGtxwsAAAAAfEFChNuJaWdU8R/x7GUP3o44600xL/0IC/SH/5J1561Nd2gB' + b'AAAAAEdcAAAAAAAAAQAAAAAAAAD4rccLAAAAAE13aAEAAAAAR1wAAAAAAAABAAAAAAAAAPitxwsAAAAA' + b'fcK1rXWbYoQKtCq2nzJiCmvpYCTjfvXYuWgji0GQsGpwcWgBAAAAAOAuAAAAAAAAAQAAAAAAAAD5rccL' + b'AAAAAHBxaAEAAAAA4C4AAAAAAAABAAAAAAAAAPmtxwsAAAAAh2GV5NQWzsgLKj06RBPx0QCB97kCA1OV' + b'UrDxEcZNvhhKd2gBAAAAANCPAAAAAAAAAQAAAAAAAAD4rccLAAAAAEp3aAEAAAAA0I8AAAAAAAABAAAA' + b'AAAAAPitxwsAAAAAibazYiCMITlc2drXqvTlt3fSCnk7W1heG3EouJogjZd4eGgBAAAAAGfNAQAAAAAA' + b'AQAAAAAAAAD8rccLAAAAAHh4aAEAAAAAZ80BAAAAAAABAAAAAAAAAPytxwsAAAAAi0AFlC/4wcwisiCx' + b'v13ss5/vcrirPwLzrSXGpy8fewyuZ2gBAAAAAN+IAAAAAAAAAQAAAAAAAAD/rccLAAAAAK5naAEAAAAA' + b'34gAAAAAAAABAAAAAAAAAP+txwsAAAAArU0itxPC4r5fWaGWOzot71pBjR2EcS+WEjK4Bzkzs3Crc2gB' + b'AAAAAPdaAAAAAAAAAQAAAAAAAAD6rccLAAAAAKtzaAEAAAAA91oAAAAAAAABAAAAAAAAAPqtxwsAAAAA' + b'vFRslRVZlbwHP1fHn9TC4H0gHT4cvadEJLsMYazqQb5kpmgBAAAAAG/6AAAAAAAAAQAAAAAAAADxrccL' + b'AAAAAGSmaAEAAAAAb/oAAAAAAAABAAAAAAAAAPGtxwsAAAAAxeks08X3OzuidkIc+gZFbXnnuyIHgNNb' + b'7PpPEpd/qijgKWMBAAAAAHBvAgAAAAAAAQAAAAAAAACdb60LAAAAAOApYwEAAAAAcG8CAAAAAAABAAAA' + b'AAAAAJ1vrQsAAAAA0HoGOdUHEoMy5c1/vlS8fo3SBHH6TZX9zKxofXfx7YOddWgBAAAAAKS2AwAAAAAA' + b'AQAAAAAAAAD6rccLAAAAAJ11aAEAAAAApLYDAAAAAAABAAAAAAAAAPqtxwsAAAAA0sj8lXSClC3CIjOA' + b'kkwaV8JH5xFY0ct7hVWuwlD+R7jNdmgBAAAAABdxAQAAAAAAAQAAAAAAAAD7rccLAAAAAM12aAEAAAAA' + b'F3EBAAAAAAABAAAAAAAAAPutxwsAAAAA1S855pC2mSbP8jFQCvvqX3MpTOXs5/BYHTl/r3O5RalFe2gB' + b'AAAAABBLAAAAAAAAAQAAAAAAAAD+rccLAAAAAEV7aAEAAAAAEEsAAAAAAAABAAAAAAAAAP6txwsAAAAA' + b'4nYQ5DOiRvjmu8YoeyW1DLXF7pdywpOP4PGqQglGiGhkc2gBAAAAAPUBAAAAAAAAAQAAAAAAAAD7rccL' + b'AAAAAGRzaAEAAAAA9QEAAAAAAAABAAAAAAAAAPutxwsAAAAA4tX2SZD3l3FAyKYJNSbTLHNOH4n15gMg' + b'uh53FIMjwtkzjGgBAAAAAHwvAAAAAAAAAQAAAAAAAAD1rccLAAAAADOMaAEAAAAAfC8AAAAAAAABAAAA' + b'AAAAAPWtxwsAAAAA6RNLhwN/jdZo6gCE/jH7lRcwks1xI3vR8WRwtPd0ihQbcGgBAAAAAOwZAAAAAAAA' + b'AQAAAAAAAAD6rccLAAAAABtwaAEAAAAA7BkAAAAAAAABAAAAAAAAAPqtxwsAAAAA75mJdHHcLE7j7fIP' + b'srWt70W9Qm7X3gxVvFw7bbzVMJJrfWgBAAAAAOUrAAAAAAAAAQAAAAAAAAD6rccLAAAAAGt9aAEAAAAA' + b'5SsAAAAAAAABAAAAAAAAAPqtxwsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7xHUr72YBwAAAAAA' + b'AAAAAJNnayVdBgAAAAAAAAAAAACBeRAFAAAAAAAAAAAAAAAA' +)) @pytest.fixture def price_account(solana_client: SolanaClient) -> PythPriceAccount: @@ -64,53 +238,54 @@ def price_account(solana_client: SolanaClient) -> PythPriceAccount: ) def test_price_account_update_from(price_account_bytes: bytes, price_account: PythPriceAccount): - price_account.update_from(buffer=price_account_bytes, version=2, offset=0) + price_account.update_from(buffer=price_account_bytes, version=2, offset=ACCOUNT_HEADER_BYTES) assert price_account.price_type == PythPriceType.PRICE - assert price_account.exponent == -8 - assert price_account.num_components == 19 + assert price_account.exponent == -5 + assert price_account.num_components == 29 assert len(price_account.price_components) == price_account.num_components - assert price_account.last_slot == 106080731 - assert price_account.valid_slot == 106080730 + assert price_account.last_slot == 197635583 + assert price_account.valid_slot == 197635582 assert price_account.product_account_key == SolanaPublicKey( - "5uKdRzB3FzdmwyCHrqSGq4u2URja617jqtKkM71BVrkw" + "3mkwqdkawySvAm1VjD4f2THN5mmXzb76fvft2hWpAANo" ) + assert price_account.max_latency == 50 assert price_account.next_price_account_key is None assert asdict(price_account.aggregate_price_info) == { - "raw_price": 70712500000, - "raw_confidence_interval": 36630500, + "raw_price": 23623373, + "raw_confidence_interval": 14454, "price_status": PythPriceStatus.TRADING, - "pub_slot": 106080731, - "exponent": -8, - "price": 707.125, - "confidence_interval": 0.366305, + "pub_slot": 197635583, + "exponent": -5, + "price": 236.23373, + "confidence_interval": 0.14454, } # Only assert the first element of the 19 price components assert asdict(price_account.price_components[0]) == { "publisher_key": SolanaPublicKey( - "HekM1hBawXQu6wK6Ah1yw1YXXeMUDD2bfCHEzo25vnEB" + "JTmFx5zX9mM94itfk2nQcJnQQDPjcv4UPD7SYj6xDCV" ), "last_aggregate_price_info": { - "raw_price": 70709500000, - "raw_confidence_interval": 21500000, + "raw_price": 23622000, + "raw_confidence_interval": 50363, "price_status": PythPriceStatus.TRADING, - "pub_slot": 106080728, - "exponent": -8, - "price": 707.095, - "confidence_interval": 0.215, + "pub_slot": 197635580, + "exponent": -5, + "price": 236.22000000000003, + "confidence_interval": 0.50363, }, "latest_price_info": { - "raw_price": 70709500000, - "raw_confidence_interval": 21500000, + "raw_price": 23622000, + "raw_confidence_interval": 50363, "price_status": PythPriceStatus.TRADING, - "pub_slot": 106080729, - "exponent": -8, - "price": 707.095, - "confidence_interval": 0.215, + "pub_slot": 197635580, + "exponent": -5, + "price": 236.22000000000003, + "confidence_interval": 0.50363, }, - "exponent": -8, + "exponent": -5, } - assert price_account.min_publishers == 0 + assert price_account.min_publishers == 3 def test_price_account_str( @@ -120,7 +295,7 @@ def test_price_account_str( assert str(price_account) == expected_empty expected = "PythPriceAccount PythPriceType.PRICE (5ALDzwcRJfSyGdGyhP3kP628aqBNHZzLuVww7o9kdspe)" - price_account.update_from(buffer=price_account_bytes, version=2, offset=0) + price_account.update_from(buffer=price_account_bytes, version=2, offset=ACCOUNT_HEADER_BYTES) assert str(price_account) == expected price_account.product = PythProductAccount( @@ -139,7 +314,7 @@ def test_price_account_str( def test_price_account_agregate_conf_interval( price_account_bytes: bytes, price_account: PythPriceAccount, ): - price_account.update_from(buffer=price_account_bytes, version=2, offset=0) + price_account.update_from(buffer=price_account_bytes, version=2, offset=ACCOUNT_HEADER_BYTES) price_account.slot = price_account.aggregate_price_info.pub_slot assert price_account.aggregate_price_confidence_interval == 0.366305 @@ -147,14 +322,14 @@ def test_price_account_agregate_conf_interval( def test_price_account_agregate_price( price_account_bytes: bytes, price_account: PythPriceAccount, ): - price_account.update_from(buffer=price_account_bytes, version=2, offset=0) + price_account.update_from(buffer=price_account_bytes, version=2, offset=ACCOUNT_HEADER_BYTES) price_account.slot = price_account.aggregate_price_info.pub_slot assert price_account.aggregate_price == 707.125 def test_price_account_unknown_status( price_account_bytes: bytes, price_account: PythPriceAccount, ): - price_account.update_from(buffer=price_account_bytes, version=2, offset=0) + price_account.update_from(buffer=price_account_bytes, version=2, offset=ACCOUNT_HEADER_BYTES) price_account.slot = price_account.aggregate_price_info.pub_slot price_account.aggregate_price_info.price_status = PythPriceStatus.UNKNOWN @@ -164,8 +339,8 @@ def test_price_account_unknown_status( def test_price_account_get_aggregate_price_status_still_trading( price_account_bytes: bytes, price_account: PythPriceAccount ): - price_account.update_from(buffer=price_account_bytes, version=2, offset=0) - price_account.slot = price_account.aggregate_price_info.pub_slot + MAX_SLOT_DIFFERENCE + price_account.update_from(buffer=price_account_bytes, version=2, offset=ACCOUNT_HEADER_BYTES) + price_account.slot = price_account.aggregate_price_info.pub_slot + DEFAULT_MAX_LATENCY price_status = price_account.aggregate_price_status assert price_status == PythPriceStatus.TRADING @@ -173,8 +348,8 @@ def test_price_account_get_aggregate_price_status_still_trading( def test_price_account_get_aggregate_price_status_got_stale( price_account_bytes: bytes, price_account: PythPriceAccount ): - price_account.update_from(buffer=price_account_bytes, version=2, offset=0) - price_account.slot = price_account.aggregate_price_info.pub_slot + MAX_SLOT_DIFFERENCE + 1 + price_account.update_from(buffer=price_account_bytes, version=2, offset=ACCOUNT_HEADER_BYTES) + price_account.slot = price_account.aggregate_price_info.pub_slot + DEFAULT_MAX_LATENCY + 1 price_status = price_account.aggregate_price_status assert price_status == PythPriceStatus.UNKNOWN diff --git a/tests/test_pyth_client.py b/tests/test_pyth_client.py index 7c467e3..4bc8ad9 100644 --- a/tests/test_pyth_client.py +++ b/tests/test_pyth_client.py @@ -3,7 +3,7 @@ import base64 from pythclient.exceptions import NotLoadedException from pythclient.pythaccounts import ( - _ACCOUNT_HEADER_BYTES, _VERSION_2, PythMappingAccount, PythPriceType, PythProductAccount, PythPriceAccount + ACCOUNT_HEADER_BYTES, _VERSION_2, PythMappingAccount, PythPriceType, PythProductAccount, PythPriceAccount ) from pythclient.pythclient import PythClient, WatchSession @@ -234,7 +234,7 @@ def product_account(solana_client: SolanaClient) -> PythProductAccount: @ pytest.fixture def product_account_bytes() -> bytes: - return base64.b64decode(PRODUCT_ACCOUNT_B64_DATA)[_ACCOUNT_HEADER_BYTES:] + return base64.b64decode(PRODUCT_ACCOUNT_B64_DATA)[ACCOUNT_HEADER_BYTES:] @ pytest.fixture @@ -247,7 +247,7 @@ def price_account(solana_client: SolanaClient) -> PythPriceAccount: @ pytest.fixture def price_account_bytes() -> bytes: - return base64.b64decode(PRICE_ACCOUNT_B64_DATA)[_ACCOUNT_HEADER_BYTES:] + return base64.b64decode(PRICE_ACCOUNT_B64_DATA)[ACCOUNT_HEADER_BYTES:] @pytest.fixture() From 235303bca9356ff4262bfbbc744329cd54ff6126 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Wed, 12 Feb 2025 15:44:14 -0800 Subject: [PATCH 4/6] fix: update tests after changing fixtures (#60) --- tests/test_price_account.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_price_account.py b/tests/test_price_account.py index 62e7c02..847b813 100644 --- a/tests/test_price_account.py +++ b/tests/test_price_account.py @@ -4,7 +4,6 @@ from pythclient.pythaccounts import ( ACCOUNT_HEADER_BYTES, - DEFAULT_MAX_LATENCY, PythPriceAccount, PythPriceType, PythPriceStatus, @@ -14,7 +13,7 @@ # Yes, this sucks, but it is actually a monster datastructure -# Equity.US.AAPL/USD symbol +# Equity.US.AAPL/USD symbol with a max latency of 50 slots @pytest.fixture def price_account_bytes(): return base64.b64decode(( @@ -316,7 +315,7 @@ def test_price_account_agregate_conf_interval( ): price_account.update_from(buffer=price_account_bytes, version=2, offset=ACCOUNT_HEADER_BYTES) price_account.slot = price_account.aggregate_price_info.pub_slot - assert price_account.aggregate_price_confidence_interval == 0.366305 + assert price_account.aggregate_price_confidence_interval == 0.14454 def test_price_account_agregate_price( @@ -324,7 +323,7 @@ def test_price_account_agregate_price( ): price_account.update_from(buffer=price_account_bytes, version=2, offset=ACCOUNT_HEADER_BYTES) price_account.slot = price_account.aggregate_price_info.pub_slot - assert price_account.aggregate_price == 707.125 + assert price_account.aggregate_price == 236.23373 def test_price_account_unknown_status( price_account_bytes: bytes, price_account: PythPriceAccount, @@ -340,7 +339,7 @@ def test_price_account_get_aggregate_price_status_still_trading( price_account_bytes: bytes, price_account: PythPriceAccount ): price_account.update_from(buffer=price_account_bytes, version=2, offset=ACCOUNT_HEADER_BYTES) - price_account.slot = price_account.aggregate_price_info.pub_slot + DEFAULT_MAX_LATENCY + price_account.slot = price_account.aggregate_price_info.pub_slot + 50 price_status = price_account.aggregate_price_status assert price_status == PythPriceStatus.TRADING @@ -349,7 +348,7 @@ def test_price_account_get_aggregate_price_status_got_stale( price_account_bytes: bytes, price_account: PythPriceAccount ): price_account.update_from(buffer=price_account_bytes, version=2, offset=ACCOUNT_HEADER_BYTES) - price_account.slot = price_account.aggregate_price_info.pub_slot + DEFAULT_MAX_LATENCY + 1 + price_account.slot = price_account.aggregate_price_info.pub_slot + 50 + 1 price_status = price_account.aggregate_price_status assert price_status == PythPriceStatus.UNKNOWN From a11655b660c9a33a72be666a19f32d25d92ff512 Mon Sep 17 00:00:00 2001 From: Adam WEAVER <43756933+aweaver89@users.noreply.github.com> Date: Mon, 12 May 2025 15:15:31 +0200 Subject: [PATCH 5/6] Update Pythnet endpoints to use api2 --- pythclient/solana.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythclient/solana.py b/pythclient/solana.py index e3c3e35..bfc6f9d 100644 --- a/pythclient/solana.py +++ b/pythclient/solana.py @@ -20,8 +20,8 @@ TESTNET_ENDPOINT = "api.testnet.solana.com" MAINNET_ENDPOINT = "api.mainnet-beta.solana.com" PYTHNET_ENDPOINT = "pythnet.rpcpool.com" -PYTHTEST_CROSSCHAIN_ENDPOINT = "api.pythtest.pyth.network" -PYTHTEST_CONFORMANCE_ENDPOINT = "api.pythtest.pyth.network" +PYTHTEST_CROSSCHAIN_ENDPOINT = "api2.pythtest.pyth.network" +PYTHTEST_CONFORMANCE_ENDPOINT = "api2.pythtest.pyth.network" SOLANA_DEVNET_WS_ENDPOINT = WS_PREFIX + "://" + DEVNET_ENDPOINT SOLANA_DEVNET_HTTP_ENDPOINT = HTTP_PREFIX + "://" + DEVNET_ENDPOINT From 552b19ecdabb0ee943af0e1b7f604a6ccd50ad93 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Fri, 16 May 2025 14:45:52 +0200 Subject: [PATCH 6/6] chore: cut release for 0.2.2 (#64) * chore: cut release for 0.2.2 * fix ci --- .github/workflows/pytest.yml | 2 +- examples/dump.py | 1 - setup.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 2f89004..94b1305 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.9"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 diff --git a/examples/dump.py b/examples/dump.py index 5704b10..8bec6f3 100644 --- a/examples/dump.py +++ b/examples/dump.py @@ -32,7 +32,6 @@ def set_to_exit(sig: Any, frame: Any): async def main(): - global to_exit use_program = len(sys.argv) >= 2 and sys.argv[1] == "program" v2_first_mapping_account_key = get_key("pythnet", "mapping") v2_program_key = get_key("pythnet", "program") diff --git a/setup.py b/setup.py index 339a2f0..8b51667 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='pythclient', - version='0.2.1', + version='0.2.2', packages=['pythclient'], author='Pyth Developers', author_email='contact@pyth.network',