Skip to content

Commit d43e9f7

Browse files
committed
Add optional weekend into market status
1 parent 91c62a2 commit d43e9f7

4 files changed

Lines changed: 224 additions & 3 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"pino-pretty": "13.1.2",
1616
"prom-client": "15.1.3",
1717
"redlock": "5.0.0-beta.2",
18-
"ws": "8.18.3"
18+
"ws": "8.18.3",
19+
"@date-fns/tz": "1.4.1"
1920
},
2021
"scripts": {
2122
"build": "rm -rf dist/src && mkdir -p ./dist/src && cp package.json dist/src && cp README.md dist/src && tsc && yarn pre-build-generator",

src/adapter/market-status.ts

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { TransportGenerics } from '../transports'
22
import { AdapterEndpoint } from './endpoint'
3-
3+
import { AdapterEndpointParams } from './types'
4+
import { TZDate } from '@date-fns/tz'
45
/**
56
* Base input parameter config that any [[MarketStatusEndpoint]] must extend
67
*/
@@ -17,6 +18,11 @@ export const marketStatusEndpointInputParametersDefinition = {
1718
options: ['regular', '24/5'],
1819
default: 'regular',
1920
},
21+
weekend: {
22+
type: 'string',
23+
description:
24+
'DHH-DHH:TZ, 520-020:America/New_York means Fri 20:00 to Sun 20:00 Eastern Time Zone',
25+
},
2026
} as const
2127

2228
export enum MarketStatus {
@@ -58,4 +64,67 @@ export type MarketStatusEndpointGenerics = TransportGenerics & {
5864
*/
5965
export class MarketStatusEndpoint<
6066
T extends MarketStatusEndpointGenerics,
61-
> extends AdapterEndpoint<T> {}
67+
> extends AdapterEndpoint<T> {
68+
constructor(params: AdapterEndpointParams<T>) {
69+
params.customInputValidation = (req, _adapterSettings) => {
70+
const data = req.requestContext.data as Record<string, string>
71+
if (data['type'] === '24/5') {
72+
validateWeekend(data['weekend'])
73+
}
74+
return undefined
75+
}
76+
super(params)
77+
}
78+
}
79+
80+
export const validateWeekend = (weekend?: string) => {
81+
const dayHour = /[0-6](0\d|1\d|2[0-3])/
82+
const timezonePattern = /[^\s]+/
83+
const regex = new RegExp(`^(${dayHour.source})-(${dayHour.source}):(${timezonePattern.source})$`)
84+
85+
const match = weekend?.match(regex)
86+
if (!match) {
87+
throw new Error(`${weekend} does not match format`)
88+
}
89+
90+
// eslint-disable-next-line new-cap
91+
Intl.DateTimeFormat(undefined, { timeZone: weekend?.split(':')[1] })
92+
}
93+
94+
export const isWeekend = (weekend?: string) => {
95+
validateWeekend(weekend)
96+
97+
// Weekend looks like 520-020:America/New_York
98+
const [range, tz] = (weekend || '').split(':')
99+
const [start, end] = range.split('-')
100+
101+
const startDay = Number(start[0])
102+
const startHour = Number(start.slice(1))
103+
const endDay = Number(end[0])
104+
const endHour = Number(end.slice(1))
105+
106+
const nowDay = TZDate.tz(tz).getDay()
107+
const nowHour = TZDate.tz(tz).getHours()
108+
109+
// Case 1: weekend does NOT wrap around the week
110+
if (startDay < endDay || (startDay === endDay && startHour < endHour)) {
111+
if (nowDay < startDay || nowDay > endDay) {
112+
return false
113+
} else if (nowDay === startDay && nowHour < startHour) {
114+
return false
115+
} else if (nowDay === endDay && nowHour >= endHour) {
116+
return false
117+
}
118+
return true
119+
}
120+
121+
// Case 2: weekend wraps around (e.g. Fri → Sun)
122+
if (nowDay > startDay || nowDay < endDay) {
123+
return true
124+
} else if (nowDay === startDay && nowHour >= startHour) {
125+
return true
126+
} else if (nowDay === endDay && nowHour < endHour) {
127+
return true
128+
}
129+
return false
130+
}

test/adapter/market-status.test.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Import from index first to ensure modules load in correct order
2+
import '../../src/adapter'
3+
4+
import test from 'ava'
5+
import { validateWeekend, isWeekend } from '../../src/adapter/market-status'
6+
import FakeTimers from '@sinonjs/fake-timers'
7+
8+
test('validateWeekend - success', (t) => {
9+
t.notThrows(() => {
10+
validateWeekend('520-020:America/New_York')
11+
validateWeekend('000-123:UTC')
12+
validateWeekend('123-423:Europe/London')
13+
validateWeekend('600-023:Asia/Tokyo')
14+
})
15+
})
16+
17+
test('validateWeekend - bad format', (t) => {
18+
t.throws(() => {
19+
validateWeekend('520020:America/New_York')
20+
})
21+
t.throws(() => {
22+
validateWeekend('520-020America/New_York')
23+
})
24+
t.throws(() => {
25+
validateWeekend('520-020:')
26+
})
27+
t.throws(() => {
28+
validateWeekend('55-020:UTC')
29+
})
30+
t.throws(() => {
31+
validateWeekend('55-20:UTC')
32+
})
33+
t.throws(() => {
34+
validateWeekend('')
35+
})
36+
t.throws(() => {
37+
validateWeekend()
38+
})
39+
t.throws(() => {
40+
validateWeekend('520:020-America/New_York')
41+
})
42+
t.throws(() => {
43+
validateWeekend('520-020: ')
44+
})
45+
})
46+
47+
test('validateWeekend - bad number', (t) => {
48+
t.throws(() => {
49+
validateWeekend('720-020:UTC')
50+
})
51+
t.throws(() => {
52+
validateWeekend('524-020:UTC')
53+
})
54+
t.throws(() => {
55+
validateWeekend('525-020:UTC')
56+
})
57+
})
58+
59+
test('validateWeekend - invalid timezone', (t) => {
60+
t.throws(() => {
61+
validateWeekend('520-020:Invalid/Timezone')
62+
validateWeekend('520-020:AmericaNew_York')
63+
})
64+
})
65+
66+
const clock = FakeTimers.install({ toFake: ['Date'] })
67+
68+
test.after(() => {
69+
clock.uninstall()
70+
})
71+
72+
test('isWeekend - UTC', (t) => {
73+
// Saturday 12:00 -> 612
74+
clock.setSystemTime(new Date('2024-01-06T12:00:00Z').getTime())
75+
76+
t.false(isWeekend('000-123:UTC'), 'Before start day')
77+
t.false(isWeekend('400-500:UTC'), 'After end day')
78+
t.false(isWeekend('613-620:UTC'), 'Before start hour')
79+
t.false(isWeekend('610-612:UTC'), 'After end hour')
80+
81+
t.true(isWeekend('610-620:UTC'), 'Non-wrapping: middle of weekend should return true')
82+
t.true(isWeekend('600-023:UTC'), 'Non-wrapping: spanning multiple days should return true')
83+
t.true(isWeekend('612-615:UTC'), 'Non-wrapping: same day, at start hour should return true')
84+
85+
t.true(isWeekend('520-020:UTC'), 'Wrapping: nowDay > startDay should return true')
86+
t.true(isWeekend('400-200:UTC'), 'Wrapping: nowDay > startDay should return true')
87+
88+
t.true(isWeekend('612-020:UTC'), 'After start hour')
89+
t.true(isWeekend('520-613:UTC'), 'Before end hour')
90+
91+
t.false(isWeekend('620-610:UTC'), 'Wrapping same day: between end and start should return false')
92+
})
93+
94+
test('isWeekend - ET', (t) => {
95+
// Saturday 12:00 UTC = Saturday 07:00 EST -> 607
96+
clock.setSystemTime(new Date('2024-01-06T12:00:00Z').getTime())
97+
98+
t.false(isWeekend('000-123:America/New_York'), 'Before start day')
99+
t.false(isWeekend('400-500:America/New_York'), 'After end day')
100+
t.false(isWeekend('608-620:America/New_York'), 'Before start hour')
101+
t.false(isWeekend('605-607:America/New_York'), 'After end hour')
102+
103+
t.true(isWeekend('605-620:America/New_York'), 'Non-wrapping: middle of weekend')
104+
t.true(isWeekend('600-023:America/New_York'), 'Non-wrapping: spanning multiple days')
105+
t.true(isWeekend('607-610:America/New_York'), 'Non-wrapping: same day, at start hour ')
106+
107+
t.true(isWeekend('520-020:America/New_York'), 'Wrapping: nowDay > startDay should return true')
108+
t.true(isWeekend('400-200:America/New_York'), 'Wrapping: nowDay > startDay should return true')
109+
110+
t.true(isWeekend('607-020:America/New_York'), 'After start hour')
111+
t.true(isWeekend('520-608:America/New_York'), 'Before end hour')
112+
113+
t.false(isWeekend('620-607:America/New_York'), 'Wrapping same day: at end hour should')
114+
})
115+
116+
test('isWeekend - ET - Fri to Sun 8 to 8', (t) => {
117+
// Weekend: Fri 20:00 to Sun 20:00 ET (520-020:America/New_York)
118+
const range = '520-020:America/New_York'
119+
// Thu 21:00 ET
120+
clock.setSystemTime(new Date('2024-01-05T02:00:00Z').getTime())
121+
t.false(isWeekend(range), 'Before start day')
122+
// Fri 19:00 ET
123+
clock.setSystemTime(new Date('2024-01-06T00:00:00Z').getTime())
124+
t.false(isWeekend(range), 'On start day, before start hour')
125+
// Fri 20:00 ET
126+
clock.setSystemTime(new Date('2024-01-06T01:00:00Z').getTime())
127+
t.true(isWeekend(range), 'On start day, at start hour')
128+
// Fri 23:00 ET
129+
clock.setSystemTime(new Date('2024-01-06T04:00:00Z').getTime())
130+
t.true(isWeekend(range), 'On start day, after start hour')
131+
// Sat 12:00 ET
132+
clock.setSystemTime(new Date('2024-01-06T17:00:00Z').getTime())
133+
t.true(isWeekend(range), 'Middle day (Saturday)')
134+
// Sun 19:00 ET
135+
clock.setSystemTime(new Date('2024-01-08T00:00:00Z').getTime())
136+
t.true(isWeekend(range), 'On end day, before end hour')
137+
// Sun 20:00 ET
138+
clock.setSystemTime(new Date('2024-01-08T01:00:00Z').getTime())
139+
t.false(isWeekend(range), 'On end day, at end hour')
140+
// Sun 21:00 ET
141+
clock.setSystemTime(new Date('2024-01-08T02:00:00Z').getTime())
142+
t.false(isWeekend(range), 'On end day, after end hour')
143+
// Mon 10:00 ET
144+
clock.setSystemTime(new Date('2024-01-08T15:00:00Z').getTime())
145+
t.false(isWeekend(range), 'After end day')
146+
})

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
dependencies:
1515
"@jridgewell/trace-mapping" "0.3.9"
1616

17+
"@date-fns/tz@1.4.1":
18+
version "1.4.1"
19+
resolved "https://registry.yarnpkg.com/@date-fns/tz/-/tz-1.4.1.tgz#2d905f282304630e07bef6d02d2e7dbf3f0cc4e4"
20+
integrity sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==
21+
1722
"@eslint-community/eslint-utils@^4.7.0":
1823
version "4.7.0"
1924
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"

0 commit comments

Comments
 (0)