|
1 | 1 | import datetime
|
| 2 | +from zoneinfo import ZoneInfo |
2 | 3 |
|
3 |
| -import pytz |
| 4 | +NY_TZ = ZoneInfo("America/New_York") |
| 5 | +UTC_TZ = ZoneInfo("UTC") |
4 | 6 |
|
5 |
| -TZ = pytz.timezone("America/New_York") |
6 |
| - |
7 |
| -EQUITY_OPEN = datetime.time(9, 30, 0, tzinfo=TZ) |
8 |
| -EQUITY_CLOSE = datetime.time(16, 0, 0, tzinfo=TZ) |
9 |
| -EQUITY_EARLY_CLOSE = datetime.time(13, 0, 0, tzinfo=TZ) |
| 7 | +EQUITY_OPEN = datetime.time(9, 30, 0, tzinfo=NY_TZ) |
| 8 | +EQUITY_CLOSE = datetime.time(16, 0, 0, tzinfo=NY_TZ) |
| 9 | +EQUITY_EARLY_CLOSE = datetime.time(13, 0, 0, tzinfo=NY_TZ) |
10 | 10 |
|
11 | 11 | # EQUITY_HOLIDAYS and EQUITY_EARLY_HOLIDAYS will need to be updated each year
|
12 | 12 | # From https://www.nyse.com/markets/hours-calendars
|
13 | 13 | EQUITY_HOLIDAYS = [
|
14 |
| - datetime.datetime(2023, 1, 2, tzinfo=TZ).date(), |
15 |
| - datetime.datetime(2023, 1, 16, tzinfo=TZ).date(), |
16 |
| - datetime.datetime(2023, 2, 20, tzinfo=TZ).date(), |
17 |
| - datetime.datetime(2023, 4, 7, tzinfo=TZ).date(), |
18 |
| - datetime.datetime(2023, 5, 29, tzinfo=TZ).date(), |
19 |
| - datetime.datetime(2023, 6, 19, tzinfo=TZ).date(), |
20 |
| - datetime.datetime(2023, 7, 4, tzinfo=TZ).date(), |
21 |
| - datetime.datetime(2022, 9, 4, tzinfo=TZ).date(), |
22 |
| - datetime.datetime(2023, 11, 23, tzinfo=TZ).date(), |
23 |
| - datetime.datetime(2023, 12, 25, tzinfo=TZ).date(), |
| 14 | + datetime.datetime(2023, 1, 2, tzinfo=NY_TZ).date(), |
| 15 | + datetime.datetime(2023, 1, 16, tzinfo=NY_TZ).date(), |
| 16 | + datetime.datetime(2023, 2, 20, tzinfo=NY_TZ).date(), |
| 17 | + datetime.datetime(2023, 4, 7, tzinfo=NY_TZ).date(), |
| 18 | + datetime.datetime(2023, 5, 29, tzinfo=NY_TZ).date(), |
| 19 | + datetime.datetime(2023, 6, 19, tzinfo=NY_TZ).date(), |
| 20 | + datetime.datetime(2023, 7, 4, tzinfo=NY_TZ).date(), |
| 21 | + datetime.datetime(2022, 9, 4, tzinfo=NY_TZ).date(), |
| 22 | + datetime.datetime(2023, 11, 23, tzinfo=NY_TZ).date(), |
| 23 | + datetime.datetime(2023, 12, 25, tzinfo=NY_TZ).date(), |
24 | 24 | ]
|
25 | 25 | EQUITY_EARLY_HOLIDAYS = [
|
26 |
| - datetime.datetime(2023, 7, 3, tzinfo=TZ).date(), |
27 |
| - datetime.datetime(2023, 11, 24, tzinfo=TZ).date(), |
| 26 | + datetime.datetime(2023, 7, 3, tzinfo=NY_TZ).date(), |
| 27 | + datetime.datetime(2023, 11, 24, tzinfo=NY_TZ).date(), |
28 | 28 | ]
|
29 | 29 |
|
30 |
| -FX_METAL_OPEN_CLOSE_TIME = datetime.time(17, 0, 0, tzinfo=TZ) |
| 30 | +FX_METAL_OPEN_CLOSE_TIME = datetime.time(17, 0, 0, tzinfo=NY_TZ) |
31 | 31 |
|
32 | 32 | # FX_METAL_HOLIDAYS will need to be updated each year
|
33 | 33 | # From https://www.cboe.com/about/hours/fx/
|
34 | 34 | FX_METAL_HOLIDAYS = [
|
35 |
| - datetime.datetime(2023, 1, 1, tzinfo=TZ).date(), |
36 |
| - datetime.datetime(2023, 12, 25, tzinfo=TZ).date(), |
| 35 | + datetime.datetime(2023, 1, 1, tzinfo=NY_TZ).date(), |
| 36 | + datetime.datetime(2023, 12, 25, tzinfo=NY_TZ).date(), |
37 | 37 | ]
|
38 | 38 |
|
39 | 39 |
|
40 | 40 | def is_market_open(asset_type: str, dt: datetime.datetime) -> bool:
|
| 41 | + # make sure time is in NY timezone |
| 42 | + dt = dt.astimezone(NY_TZ) |
41 | 43 | day, date, time = dt.weekday(), dt.date(), dt.time()
|
42 | 44 |
|
43 | 45 | if asset_type == "equity":
|
@@ -73,10 +75,12 @@ def is_market_open(asset_type: str, dt: datetime.datetime) -> bool:
|
73 | 75 |
|
74 | 76 |
|
75 | 77 | def get_next_market_open(asset_type: str, dt: datetime.datetime) -> str:
|
| 78 | + # make sure time is in NY timezone |
| 79 | + dt = dt.astimezone(NY_TZ) |
76 | 80 | time = dt.time()
|
77 | 81 |
|
78 | 82 | if is_market_open(asset_type, dt):
|
79 |
| - return dt.astimezone(pytz.UTC).strftime("%Y-%m-%dT%H:%M:%S") + "Z" |
| 83 | + return dt.astimezone(UTC_TZ).strftime("%Y-%m-%dT%H:%M:%S") + "Z" |
80 | 84 |
|
81 | 85 | if asset_type == "equity":
|
82 | 86 | if time < EQUITY_OPEN:
|
@@ -117,4 +121,38 @@ def get_next_market_open(asset_type: str, dt: datetime.datetime) -> str:
|
117 | 121 | while not is_market_open(asset_type, next_market_open):
|
118 | 122 | next_market_open += datetime.timedelta(days=1)
|
119 | 123 |
|
120 |
| - return next_market_open.astimezone(pytz.UTC).strftime("%Y-%m-%dT%H:%M:%S") + "Z" |
| 124 | + return next_market_open.astimezone(UTC_TZ).strftime("%Y-%m-%dT%H:%M:%S") + "Z" |
| 125 | + |
| 126 | +def get_next_market_close(asset_type: str, dt: datetime.datetime) -> str: |
| 127 | + # make sure time is in NY timezone |
| 128 | + dt = dt.astimezone(NY_TZ) |
| 129 | + if not is_market_open(asset_type, dt): |
| 130 | + return dt.astimezone(UTC_TZ).strftime("%Y-%m-%dT%H:%M:%S") + "Z" |
| 131 | + |
| 132 | + if asset_type == "equity": |
| 133 | + if dt.date() in EQUITY_EARLY_HOLIDAYS: |
| 134 | + |
| 135 | + next_market_close = dt.replace( |
| 136 | + hour=EQUITY_EARLY_CLOSE.hour, |
| 137 | + minute=EQUITY_EARLY_CLOSE.minute, |
| 138 | + second=0, |
| 139 | + microsecond=0, |
| 140 | + ) |
| 141 | + else: |
| 142 | + next_market_close = dt.replace( |
| 143 | + hour=EQUITY_CLOSE.hour, |
| 144 | + minute=EQUITY_CLOSE.minute, |
| 145 | + second=0, |
| 146 | + microsecond=0, |
| 147 | + ) |
| 148 | + elif asset_type in ["fx", "metal"]: |
| 149 | + next_market_close = dt.replace( |
| 150 | + hour=FX_METAL_OPEN_CLOSE_TIME.hour, |
| 151 | + minute=FX_METAL_OPEN_CLOSE_TIME.minute, |
| 152 | + second=0, |
| 153 | + microsecond=0, |
| 154 | + ) |
| 155 | + else: # crypto markets never close |
| 156 | + return None |
| 157 | + |
| 158 | + return next_market_close.astimezone(UTC_TZ).strftime("%Y-%m-%dT%H:%M:%S") + "Z" |
0 commit comments