diff --git a/.pnp.cjs b/.pnp.cjs index 269916f1b91..b1f2b434404 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -6450,6 +6450,7 @@ const RAW_RUNTIME_STATE = ["@types/node", "npm:22.14.1"],\ ["@types/sinonjs__fake-timers", "npm:8.1.5"],\ ["@types/supertest", "npm:2.0.16"],\ + ["date-fns", "npm:4.1.0"],\ ["mock-socket", "npm:9.3.1"],\ ["nock", "npm:13.5.6"],\ ["supertest", "npm:6.2.4"],\ diff --git a/packages/composites/market-status/package.json b/packages/composites/market-status/package.json index 3bfc41274ed..f08aa3d20b3 100644 --- a/packages/composites/market-status/package.json +++ b/packages/composites/market-status/package.json @@ -41,6 +41,7 @@ }, "dependencies": { "@chainlink/external-adapter-framework": "2.16.1", + "date-fns": "^4.1.0", "tslib": "2.4.1" } } diff --git a/packages/composites/market-status/src/source/data/nyse-245.json b/packages/composites/market-status/src/source/data/nyse-245.json new file mode 100644 index 00000000000..6b22acf5ad7 --- /dev/null +++ b/packages/composites/market-status/src/source/data/nyse-245.json @@ -0,0 +1,178 @@ +{ + "timezone": "America/New_York", + "weekly": [ + { + "status": "WEEKEND", + "when": [ + { + "days": ["FRIDAY"], + "times": [ + { + "start": "20:00:00", + "end": "24:00:00" + } + ] + }, + { + "days": ["SATURDAY"], + "times": [ + { + "start": "00:00:00", + "end": "24:00:00" + } + ] + }, + { + "days": ["SUNDAY"], + "times": [ + { + "start": "00:00:00", + "end": "20:00:00" + } + ] + } + ] + }, + { + "status": "PRE_MARKET", + "when": [ + { + "days": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"], + "times": [ + { + "start": "04:00:00", + "end": "09:30:00" + } + ] + } + ] + }, + { + "status": "REGULAR", + "when": [ + { + "days": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"], + "times": [ + { + "start": "09:30:00", + "end": "16:00:00" + } + ] + } + ] + }, + { + "status": "POST_MARKET", + "when": [ + { + "days": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"], + "times": [ + { + "start": "16:00:00", + "end": "20:00:00" + } + ] + } + ] + }, + { + "status": "OVERNIGHT", + "when": [ + { + "days": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY"], + "times": [ + { + "start": "00:00:00", + "end": "04:00:00" + }, + { + "start": "20:00:00", + "end": "24:00:00" + } + ] + }, + { + "days": ["SUNDAY"], + "times": [ + { + "start": "00:00:00", + "end": "04:00:00" + } + ] + } + ] + } + ], + "exceptions": [ + { + "status": "WEEKEND", + "start": "2026-01-18 20:00:00", + "end": "2026-01-19 20:00:00" + }, + { + "status": "WEEKEND", + "start": "2026-02-15 20:00:00", + "end": "2026-02-16 20:00:00" + }, + { + "status": "WEEKEND", + "start": "2026-02-15 20:00:00", + "end": "2026-02-16 20:00:00" + }, + { + "status": "WEEKEND", + "start": "2026-04-02 20:00:00", + "end": "2026-04-03 20:00:00" + }, + { + "status": "WEEKEND", + "start": "2026-05-24 20:00:00", + "end": "2026-05-25 20:00:00" + }, + { + "status": "WEEKEND", + "start": "2026-06-18 20:00:00", + "end": "2026-06-19 20:00:00" + }, + { + "status": "WEEKEND", + "start": "2026-07-02 20:00:00", + "end": "2026-07-03 20:00:00" + }, + { + "status": "WEEKEND", + "start": "2026-09-06 20:00:00", + "end": "2026-09-07 20:00:00" + }, + { + "status": "WEEKEND", + "start": "2026-11-25 20:00:00", + "end": "2026-11-26 20:00:00" + }, + { + "status": "POST_MARKET", + "start": "2026-11-27 13:00:00", + "end": "2026-11-27 17:00:00" + }, + { + "status": "WEEKEND", + "start": "2026-11-27 17:00:00", + "end": "2026-11-27 20:00:00" + }, + { + "status": "POST_MARKET", + "start": "2026-12-24 13:00:00", + "end": "2026-12-24 17:00:00" + }, + { + "status": "WEEKEND", + "start": "2026-12-24 17:00:00", + "end": "2026-12-25 20:00:00" + }, + { + "status": "WEEKEND", + "start": "2026-12-31 20:00:00", + "end": "2027-01-01 20:00:00" + } + ] +} diff --git a/packages/composites/market-status/src/source/static-nyse-245.ts b/packages/composites/market-status/src/source/static-nyse-245.ts index 3412e676566..a4576fe3dec 100644 --- a/packages/composites/market-status/src/source/static-nyse-245.ts +++ b/packages/composites/market-status/src/source/static-nyse-245.ts @@ -1,6 +1,8 @@ import { TwentyfourFiveMarketStatus } from '@chainlink/external-adapter-framework/adapter' import { isWeekendNow } from '@chainlink/external-adapter-framework/validation/market-status' import { TZDate } from '@date-fns/tz' +import { addHours, format, parse, startOfDay } from 'date-fns' +import scheduleData from './data/nyse-245.json' import { HALF_HOUR, HOUR, Month, tzDate } from './utils' const TZ = 'America/New_York' @@ -74,6 +76,60 @@ const HOLIDAY_SCHEDULE = [ ] export const getStatus = (weekend?: string) => { + const oldStatus = getStatusOld(weekend) + const newStatus = getStatusNew() + if (oldStatus.marketStatus !== newStatus) { + const now = TZDate.tz(TZ) + throw new Error( + `Market status mismatch on ${now.toString()} between old and new logic: old=${ + oldStatus.statusString + }, new=${TwentyfourFiveMarketStatus[newStatus]}`, + ) + } + return oldStatus +} + +const parseTime = (time: string, date: TZDate): TZDate => { + if (time === '24:00:00') { + const startOfDate = startOfDay(date) + return addHours(startOfDate, 24) + } + return parse(time, 'HH:mm:ss', date) +} + +const getStatusNew = () => { + const timezone = scheduleData.timezone + const now = TZDate.tz(timezone) + const nowFormatted = format(now, 'yyyy-MM-dd HH:mm:ss') + + for (const e of scheduleData.exceptions) { + if (e.start <= nowFormatted && nowFormatted < e.end) { + return TwentyfourFiveMarketStatus[e.status as keyof typeof TwentyfourFiveMarketStatus] + } + } + + const dayOfWeek = format(now, 'EEEE').toUpperCase() + for (const weekly of scheduleData.weekly) { + for (const when of weekly.when) { + if (!when.days.includes(dayOfWeek)) { + continue + } + for (const times of when.times) { + const startTime = parseTime(times.start, now) + const endTime = parseTime(times.end, now) + if (startTime <= now && now < endTime) { + return TwentyfourFiveMarketStatus[ + weekly.status as keyof typeof TwentyfourFiveMarketStatus + ] + } + } + } + } + + throw new Error(`No market status found for current time: ${nowFormatted}`) +} + +const getStatusOld = (weekend?: string) => { const now = TZDate.tz(TZ) const holiday = HOLIDAY_SCHEDULE.find( diff --git a/yarn.lock b/yarn.lock index 0c0ce34c966..ecd64901043 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3960,6 +3960,7 @@ __metadata: "@types/node": "npm:22.14.1" "@types/sinonjs__fake-timers": "npm:8.1.5" "@types/supertest": "npm:2.0.16" + date-fns: "npm:^4.1.0" mock-socket: "npm:9.3.1" nock: "npm:13.5.6" supertest: "npm:6.2.4"