Skip to content

Commit b99a905

Browse files
authored
calendar metal early close (#52)
* support metal early close * bump * reset formatting
1 parent 9e6842c commit b99a905

File tree

4 files changed

+590
-36
lines changed

4 files changed

+590
-36
lines changed

pythclient/calendar.py

+133-18
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,41 @@
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+
5579
RATES_OPEN = datetime.time(8, 0, 0, tzinfo=NY_TZ)
5680
RATES_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

Comments
 (0)