Skip to content

Commit 030aa22

Browse files
authored
Add asian hours and ice energy (#4886)
* Add asian hours * Comments * Typo * Remove duplicate
1 parent 69c53a0 commit 030aa22

15 files changed

Lines changed: 1076 additions & 11 deletions

.changeset/fruity-planes-listen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/market-status-adapter': patch
3+
---
4+
5+
Add asian hours

packages/composites/market-status/src/source/sources.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,27 +51,31 @@ const marketSources: Record<string, { primary: SourceName; secondary: SourceName
5151
},
5252
tpex: {
5353
primary: 'TRADINGHOURS',
54-
secondary: 'FINNHUB_SECONDARY',
54+
secondary: 'STATIC_TPEX',
5555
},
5656
twse: {
5757
primary: 'TRADINGHOURS',
58-
secondary: 'FINNHUB_SECONDARY',
58+
secondary: 'STATIC_TWSE',
5959
},
6060
krx: {
6161
primary: 'TRADINGHOURS',
62-
secondary: 'FINNHUB_SECONDARY',
62+
secondary: 'STATIC_KRX',
6363
},
6464
jpx: {
6565
primary: 'TRADINGHOURS',
66-
secondary: 'FINNHUB_SECONDARY',
66+
secondary: 'STATIC_JPX',
6767
},
6868
sse: {
6969
primary: 'TRADINGHOURS',
70-
secondary: 'FINNHUB_SECONDARY',
70+
secondary: 'STATIC_SSE',
7171
},
7272
szse: {
7373
primary: 'TRADINGHOURS',
74-
secondary: 'FINNHUB_SECONDARY',
74+
secondary: 'STATIC_SZSE',
75+
},
76+
ice_europe_energy: {
77+
primary: 'TRADINGHOURS',
78+
secondary: 'STATIC_ICE_EUROPE_ENERGY',
7579
},
7680
}
7781

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// ICE Brent crude trading primarily follows UK daylight savings (BST) for its core hours,
2+
// but it temporarily adjusts to conform to US daylight savings time during
3+
// the transition periods in March and October.
4+
// Because the US and UK start/end daylight savings on different dates,
5+
6+
import { MarketStatus } from '@chainlink/external-adapter-framework/adapter/market-status'
7+
import { isWeekendNow } from '@chainlink/external-adapter-framework/validation/market-status'
8+
import { TZDate } from '@date-fns/tz'
9+
import { Month, tzDate } from './utils'
10+
11+
// ICE often issues circulars outlining temporary trading hours to align with US markets.
12+
const TZ = 'America/New_York'
13+
const weekend = `518-018:${TZ}` // Friday 18:00 - Sunday 18:00
14+
15+
const HOLIDAY_SCHEDULE = [
16+
{
17+
start: tzDate(2026, Month.May, 25, 13, 30, TZ),
18+
end: tzDate(2026, Month.May, 25, 20, 0, TZ),
19+
status: MarketStatus.CLOSED,
20+
},
21+
{
22+
start: tzDate(2026, Month.Jun, 19, 13, 30, TZ),
23+
end: tzDate(2026, Month.Jun, 21, 18, 0, TZ),
24+
status: MarketStatus.CLOSED,
25+
},
26+
{
27+
start: tzDate(2026, Month.Jul, 3, 13, 30, TZ),
28+
end: tzDate(2026, Month.Jul, 5, 18, 0, TZ),
29+
status: MarketStatus.CLOSED,
30+
},
31+
{
32+
start: tzDate(2026, Month.Sep, 7, 13, 30, TZ),
33+
end: tzDate(2026, Month.Sep, 7, 20, 0, TZ),
34+
status: MarketStatus.CLOSED,
35+
},
36+
{
37+
start: tzDate(2026, Month.Nov, 26, 13, 30, TZ),
38+
end: tzDate(2026, Month.Nov, 26, 20, 0, TZ),
39+
status: MarketStatus.CLOSED,
40+
},
41+
{
42+
start: tzDate(2026, Month.Nov, 27, 15, 0, TZ),
43+
end: tzDate(2026, Month.Nov, 28, 18, 0, TZ),
44+
status: MarketStatus.CLOSED,
45+
},
46+
{
47+
start: tzDate(2026, Month.Dec, 24, 14, 0, TZ),
48+
end: tzDate(2026, Month.Dec, 27, 18, 0, TZ),
49+
status: MarketStatus.CLOSED,
50+
},
51+
{
52+
start: tzDate(2026, Month.Dec, 31, 15, 0, TZ),
53+
end: tzDate(2027, Month.Jan, 3, 18, 0, TZ),
54+
status: MarketStatus.CLOSED,
55+
},
56+
]
57+
58+
// Open 18:00 - 18:00 Sun-Fri, pause from 18:00 - 20:00 on Mon-Thu ET
59+
export const getStatus = () => {
60+
const now = TZDate.tz(TZ)
61+
62+
const holiday = HOLIDAY_SCHEDULE.find(
63+
(s) => now.getTime() >= s.start.getTime() && now.getTime() < s.end.getTime(),
64+
)
65+
if (holiday) {
66+
return {
67+
marketStatus: holiday.status,
68+
statusString: MarketStatus[holiday.status],
69+
providerIndicatedTimeUnixMs: now.getTime(),
70+
}
71+
}
72+
73+
const status =
74+
isWeekendNow(weekend) || (now.getHours() >= 18 && now.getHours() < 20 && now.getDay() != 0)
75+
? MarketStatus.CLOSED
76+
: MarketStatus.OPEN
77+
78+
return {
79+
marketStatus: status,
80+
statusString: MarketStatus[status],
81+
providerIndicatedTimeUnixMs: now.getTime(),
82+
}
83+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { MarketStatus } from '@chainlink/external-adapter-framework/adapter'
2+
import { isWeekendNow } from '@chainlink/external-adapter-framework/validation/market-status'
3+
import { TZDate } from '@date-fns/tz'
4+
import { FIVE_MINUTES, HALF_HOUR, HOUR, Month } from './utils'
5+
6+
const TZ = 'Asia/Tokyo'
7+
const weekend = `516-109:${TZ}` // Friday 16:00 - Monday 9:00
8+
9+
const HOLIDAY_SCHEDULE = [
10+
{ month: Month.Apr, day: 29 },
11+
{ month: Month.May, day: 4 },
12+
{ month: Month.May, day: 5 },
13+
{ month: Month.May, day: 6 },
14+
{ month: Month.Jul, day: 20 },
15+
{ month: Month.Aug, day: 11 },
16+
{ month: Month.Sep, day: 21 },
17+
{ month: Month.Sep, day: 22 },
18+
{ month: Month.Sep, day: 23 },
19+
{ month: Month.Oct, day: 12 },
20+
{ month: Month.Nov, day: 3 },
21+
{ month: Month.Nov, day: 23 },
22+
{ month: Month.Dec, day: 31 },
23+
]
24+
25+
// Open 09:00 - 11:30, 12:30 - 15:25 JST Mon-Fri
26+
export const getStatus = () => {
27+
const now = TZDate.tz(TZ)
28+
29+
const holiday = HOLIDAY_SCHEDULE.find(
30+
(s) => now.getMonth() === s.month && now.getDate() === s.day,
31+
)
32+
if (holiday) {
33+
return {
34+
marketStatus: MarketStatus.CLOSED,
35+
statusString: MarketStatus[MarketStatus.CLOSED],
36+
providerIndicatedTimeUnixMs: now.getTime(),
37+
}
38+
}
39+
40+
let status = MarketStatus.CLOSED
41+
const minutes = now.getHours() * HOUR + now.getMinutes()
42+
43+
if (isWeekendNow(weekend)) {
44+
status = MarketStatus.CLOSED
45+
} else if (minutes >= 9 * HOUR && minutes < 11 * HOUR + HALF_HOUR) {
46+
status = MarketStatus.OPEN
47+
} else if (minutes >= 12 * HOUR + HALF_HOUR && minutes < 15 * HOUR + HALF_HOUR - FIVE_MINUTES) {
48+
status = MarketStatus.OPEN
49+
}
50+
51+
return {
52+
marketStatus: status,
53+
statusString: MarketStatus[status],
54+
providerIndicatedTimeUnixMs: now.getTime(),
55+
}
56+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { MarketStatus } from '@chainlink/external-adapter-framework/adapter'
2+
import { isWeekendNow } from '@chainlink/external-adapter-framework/validation/market-status'
3+
import { TZDate } from '@date-fns/tz'
4+
import { FIVE_MINUTES, HALF_HOUR, HOUR, Month, tzDate } from './utils'
5+
6+
const TZ = 'Asia/Seoul'
7+
const weekend = `516-109:${TZ}` // Friday 16:00 - Monday 9:00
8+
9+
const HOLIDAY_SCHEDULE = [
10+
{
11+
start: tzDate(2026, Month.May, 1, 9, 0, TZ),
12+
end: tzDate(2026, Month.May, 1, 16, 0, TZ),
13+
status: MarketStatus.CLOSED,
14+
},
15+
{
16+
start: tzDate(2026, Month.May, 5, 9, 0, TZ),
17+
end: tzDate(2026, Month.May, 5, 16, 0, TZ),
18+
status: MarketStatus.CLOSED,
19+
},
20+
{
21+
start: tzDate(2026, Month.May, 25, 9, 0, TZ),
22+
end: tzDate(2026, Month.May, 25, 16, 0, TZ),
23+
status: MarketStatus.CLOSED,
24+
},
25+
{
26+
start: tzDate(2026, Month.Aug, 17, 9, 0, TZ),
27+
end: tzDate(2026, Month.Aug, 17, 16, 0, TZ),
28+
status: MarketStatus.CLOSED,
29+
},
30+
{
31+
start: tzDate(2026, Month.Sep, 24, 9, 0, TZ),
32+
end: tzDate(2026, Month.Sep, 24, 16, 0, TZ),
33+
status: MarketStatus.CLOSED,
34+
},
35+
{
36+
start: tzDate(2026, Month.Sep, 25, 9, 0, TZ),
37+
end: tzDate(2026, Month.Sep, 25, 16, 0, TZ),
38+
status: MarketStatus.CLOSED,
39+
},
40+
{
41+
start: tzDate(2026, Month.Oct, 5, 9, 0, TZ),
42+
end: tzDate(2026, Month.Oct, 5, 16, 0, TZ),
43+
status: MarketStatus.CLOSED,
44+
},
45+
{
46+
start: tzDate(2026, Month.Nov, 19, 9, 0, TZ),
47+
end: tzDate(2026, Month.Nov, 19, 10, 0, TZ),
48+
status: MarketStatus.CLOSED,
49+
},
50+
{
51+
start: tzDate(2026, Month.Nov, 19, 15, 20, TZ),
52+
end: tzDate(2026, Month.Nov, 19, 16, 20, TZ),
53+
status: MarketStatus.OPEN,
54+
},
55+
{
56+
start: tzDate(2026, Month.Dec, 25, 9, 0, TZ),
57+
end: tzDate(2026, Month.Dec, 25, 16, 0, TZ),
58+
status: MarketStatus.CLOSED,
59+
},
60+
{
61+
start: tzDate(2026, Month.Dec, 31, 9, 0, TZ),
62+
end: tzDate(2026, Month.Dec, 31, 16, 0, TZ),
63+
status: MarketStatus.CLOSED,
64+
},
65+
]
66+
67+
// Open 09:00 - 15:20 KST Mon-Fri
68+
export const getStatus = () => {
69+
const now = TZDate.tz(TZ)
70+
71+
const holiday = HOLIDAY_SCHEDULE.find(
72+
(s) => now.getTime() >= s.start.getTime() && now.getTime() < s.end.getTime(),
73+
)
74+
if (holiday) {
75+
return {
76+
marketStatus: holiday.status,
77+
statusString: MarketStatus[holiday.status],
78+
providerIndicatedTimeUnixMs: now.getTime(),
79+
}
80+
}
81+
82+
let status = MarketStatus.CLOSED
83+
const minutes = now.getHours() * HOUR + now.getMinutes()
84+
85+
if (isWeekendNow(weekend)) {
86+
status = MarketStatus.CLOSED
87+
} else if (minutes >= 9 * HOUR && minutes < 15 * HOUR + HALF_HOUR - FIVE_MINUTES * 2) {
88+
status = MarketStatus.OPEN
89+
}
90+
91+
return {
92+
marketStatus: status,
93+
statusString: MarketStatus[status],
94+
providerIndicatedTimeUnixMs: now.getTime(),
95+
}
96+
}

packages/composites/market-status/src/source/static-nymex.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,6 @@ const HOLIDAY_SCHEDULE = [
4242
end: tzDate(2026, Month.Dec, 27, 17, 0, TZ),
4343
status: MarketStatus.CLOSED,
4444
},
45-
{
46-
start: tzDate(2026, Month.Dec, 24, 12, 45, TZ),
47-
end: tzDate(2026, Month.Dec, 27, 17, 0, TZ),
48-
status: MarketStatus.CLOSED,
49-
},
5045
{
5146
start: tzDate(2026, Month.Dec, 31, 16, 0, TZ),
5247
end: tzDate(2027, Month.Jan, 3, 17, 0, TZ),
@@ -55,6 +50,7 @@ const HOLIDAY_SCHEDULE = [
5550
]
5651

5752
// https://www.cmegroup.com/trading-hours.html
53+
// Open 5PM Sun to 4PM Fri CT, with daily closures from 4PM to 5PM
5854
export const getStatus = () => {
5955
const now = TZDate.tz(TZ)
6056

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { MarketStatus } from '@chainlink/external-adapter-framework/adapter'
2+
import { isWeekendNow } from '@chainlink/external-adapter-framework/validation/market-status'
3+
import { TZDate } from '@date-fns/tz'
4+
import { HALF_HOUR, HOUR, Month, ONE_MINUTE } from './utils'
5+
6+
const TZ = 'Asia/Shanghai'
7+
const weekend = `515-109:${TZ}` // Friday 15:00 - Monday 9:00
8+
9+
const HOLIDAY_SCHEDULE = [
10+
{ month: Month.May, day: 1 },
11+
{ month: Month.May, day: 2 },
12+
{ month: Month.May, day: 3 },
13+
{ month: Month.May, day: 4 },
14+
{ month: Month.May, day: 5 },
15+
{ month: Month.Jun, day: 19 },
16+
{ month: Month.Sep, day: 25 },
17+
{ month: Month.Oct, day: 1 },
18+
{ month: Month.Oct, day: 2 },
19+
{ month: Month.Oct, day: 3 },
20+
{ month: Month.Oct, day: 4 },
21+
{ month: Month.Oct, day: 5 },
22+
{ month: Month.Oct, day: 6 },
23+
{ month: Month.Oct, day: 7 },
24+
]
25+
26+
// Open 09:30 - 11:30, 13:00 - 14:57 CST Mon-Fri
27+
export const getStatus = () => {
28+
const now = TZDate.tz(TZ)
29+
30+
const holiday = HOLIDAY_SCHEDULE.find(
31+
(s) => now.getMonth() === s.month && now.getDate() === s.day,
32+
)
33+
if (holiday) {
34+
return {
35+
marketStatus: MarketStatus.CLOSED,
36+
statusString: MarketStatus[MarketStatus.CLOSED],
37+
providerIndicatedTimeUnixMs: now.getTime(),
38+
}
39+
}
40+
41+
let status = MarketStatus.CLOSED
42+
const minutes = now.getHours() * HOUR + now.getMinutes()
43+
44+
if (isWeekendNow(weekend)) {
45+
status = MarketStatus.CLOSED
46+
} else if (minutes >= 9 * HOUR + HALF_HOUR && minutes < 11 * HOUR + HALF_HOUR) {
47+
status = MarketStatus.OPEN
48+
} else if (minutes >= 13 * HOUR && minutes < 15 * HOUR - ONE_MINUTE * 3) {
49+
status = MarketStatus.OPEN
50+
}
51+
52+
return {
53+
marketStatus: status,
54+
statusString: MarketStatus[status],
55+
providerIndicatedTimeUnixMs: now.getTime(),
56+
}
57+
}

0 commit comments

Comments
 (0)