41
41
datetime .datetime (2024 , 12 , 24 , tzinfo = NY_TZ ).date (),
42
42
]
43
43
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 )
45
45
46
46
# FX_METAL_HOLIDAYS will need to be updated each year
47
47
# From https://www.cboe.com/about/hours/fx/
48
- FX_METAL_HOLIDAYS = [
48
+ FX_HOLIDAYS = [
49
49
datetime .datetime (2023 , 1 , 1 , tzinfo = NY_TZ ).date (),
50
50
datetime .datetime (2023 , 12 , 25 , tzinfo = NY_TZ ).date (),
51
51
datetime .datetime (2024 , 1 , 1 , tzinfo = NY_TZ ).date (),
52
52
datetime .datetime (2024 , 12 , 25 , tzinfo = NY_TZ ).date (),
53
53
]
54
54
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
+
55
79
RATES_OPEN = datetime .time (8 , 0 , 0 , tzinfo = NY_TZ )
56
80
RATES_CLOSE = datetime .time (17 , 0 , 0 , tzinfo = NY_TZ )
57
81
@@ -74,24 +98,48 @@ def is_market_open(asset_type: str, dt: datetime.datetime) -> bool:
74
98
return True
75
99
return False
76
100
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 :
79
103
return False
80
104
# If the next day is a holiday, the market is closed at 5pm ET
81
105
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 :
84
108
return False
85
109
# 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 :
87
111
return False
88
112
# On Saturday the market is closed all the time
89
113
if day == 5 :
90
114
return False
91
115
# 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 :
93
117
return False
118
+ return True
94
119
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
95
143
return True
96
144
97
145
if asset_type == "rates" :
@@ -132,25 +180,62 @@ def get_next_market_open(asset_type: str, dt: datetime.datetime) -> int:
132
180
microsecond = 0 ,
133
181
)
134
182
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
138
186
):
139
187
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 ,
142
190
second = 0 ,
143
191
microsecond = 0 ,
144
192
)
145
193
else :
146
194
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 ,
149
197
second = 0 ,
150
198
microsecond = 0 ,
151
199
)
152
200
while is_market_open (asset_type , next_market_open ):
153
201
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 )
154
239
elif asset_type == "rates" :
155
240
if time < RATES_OPEN :
156
241
next_market_open = dt .replace (
@@ -244,10 +329,10 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> int:
244
329
):
245
330
next_market_close += datetime .timedelta (days = 1 )
246
331
247
- elif asset_type in [ "fx" , "metal" ] :
332
+ elif asset_type == "fx" :
248
333
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 ,
251
336
second = 0 ,
252
337
microsecond = 0 ,
253
338
)
@@ -256,6 +341,36 @@ def get_next_market_close(asset_type: str, dt: datetime.datetime) -> int:
256
341
next_market_close += datetime .timedelta (days = 1 )
257
342
while is_market_open (asset_type , next_market_close ):
258
343
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 )
259
374
elif asset_type == "rates" :
260
375
if dt .date () in NYSE_EARLY_HOLIDAYS :
261
376
if time < NYSE_EARLY_CLOSE :
0 commit comments