4141 datetime .datetime (2024 , 12 , 24 , tzinfo = NY_TZ ).date (),
4242]
4343
44- FX_METAL_OPEN_CLOSE_TIME = datetime .time (17 , 0 , 0 , tzinfo = NY_TZ )
44+ FX_OPEN_CLOSE_TIME = datetime .time (17 , 0 , 0 , tzinfo = NY_TZ )
4545
4646# FX_METAL_HOLIDAYS will need to be updated each year
4747# From https://www.cboe.com/about/hours/fx/
48- FX_METAL_HOLIDAYS = [
48+ FX_HOLIDAYS = [
4949 datetime .datetime (2023 , 1 , 1 , tzinfo = NY_TZ ).date (),
5050 datetime .datetime (2023 , 12 , 25 , tzinfo = NY_TZ ).date (),
5151 datetime .datetime (2024 , 1 , 1 , tzinfo = NY_TZ ).date (),
5252 datetime .datetime (2024 , 12 , 25 , tzinfo = NY_TZ ).date (),
5353]
5454
55+ METAL_OPEN_CLOSE_TIME = datetime .time (17 , 0 , 0 , tzinfo = NY_TZ )
56+
57+
58+ # References:
59+ # https://www.forex.com/en-ca/help-and-support/market-trading-hours/
60+ METAL_EARLY_CLOSE = datetime .time (14 , 30 , 0 , tzinfo = NY_TZ )
61+
62+ # References:
63+ # https://www.ig.com/uk/help-and-support/spread-betting-and-cfds/market-details/martin-luther-king-jr-trading-hours
64+ # https://www.etoro.com/trading/market-hours-and-events/
65+ METAL_EARLY_CLOSE_OPEN = datetime .time (18 , 0 , 0 , tzinfo = NY_TZ )
66+
67+ # FX_METAL_HOLIDAYS will need to be updated each year
68+ # From https://www.cboe.com/about/hours/fx/
69+ METAL_HOLIDAYS = [
70+ datetime .datetime (2023 , 1 , 1 , tzinfo = NY_TZ ).date (),
71+ datetime .datetime (2023 , 12 , 25 , tzinfo = NY_TZ ).date (),
72+ datetime .datetime (2024 , 1 , 1 , tzinfo = NY_TZ ).date (),
73+ datetime .datetime (2024 , 12 , 25 , tzinfo = NY_TZ ).date (),
74+ ]
75+ METAL_EARLY_HOLIDAYS = [
76+ datetime .datetime (2024 , 1 , 15 , tzinfo = NY_TZ ).date (),
77+ ]
78+
5579RATES_OPEN = datetime .time (8 , 0 , 0 , tzinfo = NY_TZ )
5680RATES_CLOSE = datetime .time (17 , 0 , 0 , tzinfo = NY_TZ )
5781
@@ -74,24 +98,48 @@ def is_market_open(asset_type: str, dt: datetime.datetime) -> bool:
7498 return True
7599 return False
76100
77- if asset_type in [ "fx" , "metal" ] :
78- if date in FX_METAL_HOLIDAYS and time < FX_METAL_OPEN_CLOSE_TIME :
101+ if asset_type == "fx" :
102+ if date in FX_HOLIDAYS and time < FX_OPEN_CLOSE_TIME :
79103 return False
80104 # If the next day is a holiday, the market is closed at 5pm ET
81105 if (
82- date + datetime .timedelta (days = 1 ) in FX_METAL_HOLIDAYS
83- ) and time >= FX_METAL_OPEN_CLOSE_TIME :
106+ date + datetime .timedelta (days = 1 ) in FX_HOLIDAYS
107+ ) and time >= FX_OPEN_CLOSE_TIME :
84108 return False
85109 # On Friday the market is closed after 5pm
86- if day == 4 and time >= FX_METAL_OPEN_CLOSE_TIME :
110+ if day == 4 and time >= FX_OPEN_CLOSE_TIME :
87111 return False
88112 # On Saturday the market is closed all the time
89113 if day == 5 :
90114 return False
91115 # On Sunday the market is closed before 5pm
92- if day == 6 and time < FX_METAL_OPEN_CLOSE_TIME :
116+ if day == 6 and time < FX_OPEN_CLOSE_TIME :
93117 return False
118+ return True
94119
120+ if asset_type == "metal" :
121+ if date in METAL_HOLIDAYS and time < METAL_OPEN_CLOSE_TIME :
122+ return False
123+ # If the next day is a holiday, the market is closed at 5pm ET
124+ if (
125+ date + datetime .timedelta (days = 1 ) in METAL_HOLIDAYS
126+ ) and time >= METAL_OPEN_CLOSE_TIME :
127+ return False
128+ if (
129+ date in METAL_EARLY_HOLIDAYS
130+ and time >= METAL_EARLY_CLOSE
131+ and time < METAL_EARLY_CLOSE_OPEN
132+ ):
133+ return False
134+ # On Friday the market is closed after 5pm
135+ if day == 4 and time >= METAL_OPEN_CLOSE_TIME :
136+ return False
137+ # On Saturday the market is closed all the time
138+ if day == 5 :
139+ return False
140+ # On Sunday the market is closed before 5pm
141+ if day == 6 and time < METAL_OPEN_CLOSE_TIME :
142+ return False
95143 return True
96144
97145 if asset_type == "rates" :
@@ -132,25 +180,62 @@ def get_next_market_open(asset_type: str, dt: datetime.datetime) -> int:
132180 microsecond = 0 ,
133181 )
134182 next_market_open += datetime .timedelta (days = 1 )
135- elif asset_type in [ "fx" , "metal" ] :
136- if (dt .weekday () == 6 and time < FX_METAL_OPEN_CLOSE_TIME ) or (
137- dt .date () in FX_METAL_HOLIDAYS and time < FX_METAL_OPEN_CLOSE_TIME
183+ elif asset_type == "fx" :
184+ if (dt .weekday () == 6 and time < FX_OPEN_CLOSE_TIME ) or (
185+ dt .date () in FX_HOLIDAYS and time < FX_OPEN_CLOSE_TIME
138186 ):
139187 next_market_open = dt .replace (
140- hour = FX_METAL_OPEN_CLOSE_TIME .hour ,
141- minute = FX_METAL_OPEN_CLOSE_TIME .minute ,
188+ hour = FX_OPEN_CLOSE_TIME .hour ,
189+ minute = FX_OPEN_CLOSE_TIME .minute ,
142190 second = 0 ,
143191 microsecond = 0 ,
144192 )
145193 else :
146194 next_market_open = dt .replace (
147- hour = FX_METAL_OPEN_CLOSE_TIME .hour ,
148- minute = FX_METAL_OPEN_CLOSE_TIME .minute ,
195+ hour = FX_OPEN_CLOSE_TIME .hour ,
196+ minute = FX_OPEN_CLOSE_TIME .minute ,
149197 second = 0 ,
150198 microsecond = 0 ,
151199 )
152200 while is_market_open (asset_type , next_market_open ):
153201 next_market_open += datetime .timedelta (days = 1 )
202+ elif asset_type == "metal" :
203+ if dt .date () in METAL_EARLY_HOLIDAYS and time < METAL_EARLY_CLOSE_OPEN :
204+ next_market_open = dt .replace (
205+ hour = METAL_EARLY_CLOSE_OPEN .hour ,
206+ minute = METAL_EARLY_CLOSE_OPEN .minute ,
207+ second = 0 ,
208+ microsecond = 0 ,
209+ )
210+ elif dt .date () in METAL_EARLY_HOLIDAYS and time >= METAL_EARLY_CLOSE_OPEN :
211+ next_market_open = dt .replace (
212+ hour = METAL_OPEN_CLOSE_TIME .hour ,
213+ minute = METAL_OPEN_CLOSE_TIME .minute ,
214+ second = 0 ,
215+ microsecond = 0 ,
216+ )
217+ next_market_open += datetime .timedelta (days = 1 )
218+ while is_market_open (asset_type , next_market_open ):
219+ next_market_open += datetime .timedelta (days = 1 )
220+ else :
221+ if (dt .weekday () == 6 and time < METAL_OPEN_CLOSE_TIME ) or (
222+ dt .date () in METAL_HOLIDAYS and time < METAL_OPEN_CLOSE_TIME
223+ ):
224+ next_market_open = dt .replace (
225+ hour = METAL_OPEN_CLOSE_TIME .hour ,
226+ minute = METAL_OPEN_CLOSE_TIME .minute ,
227+ second = 0 ,
228+ microsecond = 0 ,
229+ )
230+ else :
231+ next_market_open = dt .replace (
232+ hour = METAL_OPEN_CLOSE_TIME .hour ,
233+ minute = METAL_OPEN_CLOSE_TIME .minute ,
234+ second = 0 ,
235+ microsecond = 0 ,
236+ )
237+ while is_market_open (asset_type , next_market_open ):
238+ next_market_open += datetime .timedelta (days = 1 )
154239 elif asset_type == "rates" :
155240 if time < RATES_OPEN :
156241 next_market_open = dt .replace (
@@ -244,10 +329,10 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> int:
244329 ):
245330 next_market_close += datetime .timedelta (days = 1 )
246331
247- elif asset_type in [ "fx" , "metal" ] :
332+ elif asset_type == "fx" :
248333 next_market_close = dt .replace (
249- hour = FX_METAL_OPEN_CLOSE_TIME .hour ,
250- minute = FX_METAL_OPEN_CLOSE_TIME .minute ,
334+ hour = FX_OPEN_CLOSE_TIME .hour ,
335+ minute = FX_OPEN_CLOSE_TIME .minute ,
251336 second = 0 ,
252337 microsecond = 0 ,
253338 )
@@ -256,6 +341,36 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> int:
256341 next_market_close += datetime .timedelta (days = 1 )
257342 while is_market_open (asset_type , next_market_close ):
258343 next_market_close += datetime .timedelta (days = 1 )
344+ elif asset_type == "metal" :
345+ if dt .date () in METAL_EARLY_HOLIDAYS and time < METAL_EARLY_CLOSE :
346+ next_market_close = dt .replace (
347+ hour = METAL_EARLY_CLOSE .hour ,
348+ minute = METAL_EARLY_CLOSE .minute ,
349+ second = 0 ,
350+ microsecond = 0 ,
351+ )
352+ elif dt .date () in METAL_EARLY_HOLIDAYS and time >= METAL_EARLY_CLOSE :
353+ next_market_close = dt .replace (
354+ hour = METAL_OPEN_CLOSE_TIME .hour ,
355+ minute = METAL_OPEN_CLOSE_TIME .minute ,
356+ second = 0 ,
357+ microsecond = 0 ,
358+ )
359+ next_market_close += datetime .timedelta (days = 1 )
360+ while is_market_open (asset_type , next_market_close ):
361+ next_market_close += datetime .timedelta (days = 1 )
362+ else :
363+ next_market_close = dt .replace (
364+ hour = METAL_OPEN_CLOSE_TIME .hour ,
365+ minute = METAL_OPEN_CLOSE_TIME .minute ,
366+ second = 0 ,
367+ microsecond = 0 ,
368+ )
369+ if dt .weekday () != 4 :
370+ while not is_market_open (asset_type , next_market_close ):
371+ next_market_close += datetime .timedelta (days = 1 )
372+ while is_market_open (asset_type , next_market_close ):
373+ next_market_close += datetime .timedelta (days = 1 )
259374 elif asset_type == "rates" :
260375 if dt .date () in NYSE_EARLY_HOLIDAYS :
261376 if time < NYSE_EARLY_CLOSE :
0 commit comments